feat(rbac): add roles and permissions

This commit is contained in:
Schubert Ferenc 2026-07-02 23:05:59 +02:00
parent 86a32a942c
commit 694b7bd09a
37 changed files with 2682 additions and 218 deletions

View file

@ -20,6 +20,8 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import UserFormDialog from "@/components/users/UserFormDialog";
import { api } from "@/lib/api";
import { hasPermission } from "@/lib/permissions";
import type { CurrentUser, Role } from "@/types/rbac";
import type { User, UserPayload } from "@/types/user";
function getErrorMessage(error: unknown) {
@ -41,6 +43,8 @@ function formatDateTime(value: string) {
export default function UserDetailPage() {
const params = useParams<{ id: string }>();
const [user, setUser] = useState<User | null>(null);
const [roles, setRoles] = useState<Role[]>([]);
const [currentUser, setCurrentUser] = useState<CurrentUser | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [pending, setPending] = useState(false);
@ -52,8 +56,19 @@ export default function UserDetailPage() {
const loadUser = useCallback(async () => {
try {
const response = await api.get<User>(`/users/${params.id}`);
setUser(response.data);
const [userResponse, meResponse] = await Promise.all([
api.get<User>(`/users/${params.id}`),
api.get<CurrentUser>("/me"),
]);
setUser(userResponse.data);
setCurrentUser(meResponse.data);
if (hasPermission(meResponse.data, "roles.read")) {
const rolesResponse = await api.get<Role[]>("/roles");
setRoles(rolesResponse.data);
} else {
setRoles([userResponse.data.primary_role]);
}
} catch (err) {
setError(getErrorMessage(err));
} finally {
@ -147,14 +162,18 @@ export default function UserDetailPage() {
</div>
<div className="flex gap-2">
<Button type="button" variant="outline" onClick={() => setPasswordOpen(true)}>
<KeyRound size={16} />
Passwort ändern
</Button>
<Button type="button" onClick={() => setFormOpen(true)}>
<Pencil size={16} />
Bearbeiten
</Button>
{hasPermission(currentUser, "users.password.update") && (
<Button type="button" variant="outline" onClick={() => setPasswordOpen(true)}>
<KeyRound size={16} />
Passwort ändern
</Button>
)}
{hasPermission(currentUser, "users.update") && (
<Button type="button" onClick={() => setFormOpen(true)}>
<Pencil size={16} />
Bearbeiten
</Button>
)}
</div>
</div>
@ -165,7 +184,7 @@ export default function UserDetailPage() {
<Detail label="Nachname" value={user.last_name || "-"} />
<Detail label="Benutzername" value={user.username} />
<Detail label="E-Mail" value={user.email} />
<Detail label="Rolle" value={user.role} />
<Detail label="Rolle" value={user.primary_role.display_name} />
<div>
<dt className="text-sm text-slate-500">Status</dt>
<dd className="mt-1"><StatusBadge active={user.is_active} /></dd>
@ -180,6 +199,7 @@ export default function UserDetailPage() {
user={user}
pending={pending}
serverError={formError}
roles={roles}
onOpenChange={setFormOpen}
onSubmit={saveUser}
/>