a3aff9fa49
- add `src/core/modules/` with registry, discovery (server), and public index - add `src/core/public-pages/` with registry, server component, and public index - add `src/core/users/permissions-registry.js` for runtime permission registration - expose `./modules`, `./public-pages`, and `./public-pages/server` package exports - rename `registerFeatureRoutes` to `registerApiRoutes` with backward-compatible alias - extend `seedDefaultRolesAndPermissions` to include module-registered permissions - update `initializeZen` and shared init to wire module discovery and registration - add `docs/MODULES.md` documenting the `@zen/module-*` authoring contract - update `docs/DEV.md` with references to module system docs
190 lines
7.5 KiB
JavaScript
190 lines
7.5 KiB
JavaScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Input, Textarea, Switch, Modal, ColorPicker } from '@zen/core/shared/components';
|
|
import { useToast } from '@zen/core/toast';
|
|
|
|
const RoleEditModal = ({ roleId, isOpen, onClose, onSaved }) => {
|
|
const toast = useToast();
|
|
const isNew = !roleId || roleId === 'new';
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [isSystem, setIsSystem] = useState(false);
|
|
|
|
const [name, setName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [color, setColor] = useState('#6b7280');
|
|
const [selectedPerms, setSelectedPerms] = useState([]);
|
|
// Catalogue dynamique des permissions (core + modules), récupéré via l'API.
|
|
const [permissionGroups, setPermissionGroups] = useState({});
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
fetchPermissions();
|
|
if (isNew) {
|
|
setName('');
|
|
setDescription('');
|
|
setColor('#6b7280');
|
|
setSelectedPerms([]);
|
|
setIsSystem(false);
|
|
return;
|
|
}
|
|
fetchRole();
|
|
}, [isOpen, roleId]);
|
|
|
|
const fetchPermissions = async () => {
|
|
try {
|
|
const response = await fetch('/zen/api/permissions', { credentials: 'include' });
|
|
if (!response.ok) return;
|
|
const data = await response.json();
|
|
setPermissionGroups(data.groups || {});
|
|
} catch {
|
|
// Si le catalogue n'est pas joignable, on laisse l'utilisateur sauvegarder
|
|
// ses changements ; les permissions invalides sont filtrées côté serveur.
|
|
}
|
|
};
|
|
|
|
const fetchRole = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetch(`/zen/api/roles/${roleId}`, { credentials: 'include' });
|
|
if (!response.ok) {
|
|
toast.error('Rôle introuvable');
|
|
onClose();
|
|
return;
|
|
}
|
|
const data = await response.json();
|
|
const role = data.role;
|
|
setName(role.name || '');
|
|
setDescription(role.description || '');
|
|
setColor(role.color || '#6b7280');
|
|
setSelectedPerms(role.permission_keys || []);
|
|
setIsSystem(role.is_system || false);
|
|
} catch {
|
|
toast.error('Impossible de charger ce rôle');
|
|
onClose();
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const togglePerm = (key) => {
|
|
setSelectedPerms(prev =>
|
|
prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key]
|
|
);
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!name.trim()) {
|
|
toast.error('Le nom du rôle est requis');
|
|
return;
|
|
}
|
|
try {
|
|
setSaving(true);
|
|
const url = isNew ? '/zen/api/roles' : `/zen/api/roles/${roleId}`;
|
|
const method = isNew ? 'POST' : 'PUT';
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name: name.trim(),
|
|
description: description.trim() || null,
|
|
color,
|
|
permissionKeys: selectedPerms,
|
|
}),
|
|
});
|
|
const data = await response.json();
|
|
if (!response.ok) {
|
|
toast.error(data.message || 'Impossible de sauvegarder ce rôle');
|
|
return;
|
|
}
|
|
toast.success(isNew ? 'Rôle créé' : 'Rôle mis à jour');
|
|
onSaved?.();
|
|
onClose();
|
|
} catch {
|
|
toast.error('Impossible de sauvegarder ce rôle');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const title = isNew ? 'Nouveau rôle' : `Modifier "${name}"`;
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title={title}
|
|
onSubmit={handleSubmit}
|
|
submitLabel={isNew ? 'Créer le rôle' : 'Sauvegarder'}
|
|
loading={saving}
|
|
disabled={loading}
|
|
size="lg"
|
|
>
|
|
{loading ? (
|
|
<div className="flex flex-col gap-4 animate-pulse">
|
|
<div className="h-10 bg-neutral-100 dark:bg-neutral-800 rounded-lg" />
|
|
<div className="h-20 bg-neutral-100 dark:bg-neutral-800 rounded-lg" />
|
|
<div className="h-40 bg-neutral-100 dark:bg-neutral-800 rounded-lg" />
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col gap-6">
|
|
<div className="flex flex-col gap-4">
|
|
<Input
|
|
label="Nom du rôle"
|
|
value={name}
|
|
onChange={setName}
|
|
placeholder="Éditeur, Modérateur..."
|
|
required
|
|
/>
|
|
<Textarea
|
|
label="Description"
|
|
value={description}
|
|
onChange={setDescription}
|
|
rows={2}
|
|
placeholder="Description optionnelle..."
|
|
/>
|
|
<ColorPicker
|
|
label="Couleur du rôle"
|
|
description="Les membres utilisent la couleur du rôle le plus élevé qu'ils possèdent."
|
|
value={color}
|
|
onChange={setColor}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-3">
|
|
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Permissions</p>
|
|
{Object.entries(permissionGroups).map(([group, perms]) => (
|
|
<div key={group} className="rounded-xl border border-neutral-200 dark:border-neutral-700/60 overflow-hidden">
|
|
<div className="px-4 py-2.5 bg-neutral-50 dark:bg-neutral-800/60 border-b border-neutral-200 dark:border-neutral-700/60">
|
|
<p className="text-[11px] font-medium font-ibm-plex-mono text-neutral-500 dark:text-neutral-400 uppercase tracking-wide">
|
|
{group}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-700/40">
|
|
{perms.map((perm) => (
|
|
<Switch
|
|
key={perm.key}
|
|
checked={selectedPerms.includes(perm.key)}
|
|
onChange={() => togglePerm(perm.key)}
|
|
label={perm.name}
|
|
description={perm.description}
|
|
disabled={isSystem}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default RoleEditModal;
|