feat(rbac): add roles and permissions
This commit is contained in:
parent
86a32a942c
commit
694b7bd09a
37 changed files with 2682 additions and 218 deletions
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { Button } from "@/components/ui/button";
|
|||
import UserFormDialog from "@/components/users/UserFormDialog";
|
||||
import UserTable from "@/components/users/UserTable";
|
||||
import { api } from "@/lib/api";
|
||||
import { hasAllPermissions, hasPermission } from "@/lib/permissions";
|
||||
import type { CurrentUser, Role } from "@/types/rbac";
|
||||
import type { User, UserPayload, UserRole } from "@/types/user";
|
||||
|
||||
const pageSize = 10;
|
||||
|
|
@ -31,6 +33,8 @@ function getSortableValue(user: User, key: string) {
|
|||
|
||||
export default function UsersPage() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [currentUser, setCurrentUser] = useState<CurrentUser | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [formError, setFormError] = useState("");
|
||||
|
|
@ -48,8 +52,22 @@ export default function UsersPage() {
|
|||
|
||||
const loadUsers = useCallback(async () => {
|
||||
try {
|
||||
const response = await api.get<User[]>("/users");
|
||||
setUsers(response.data);
|
||||
const [usersResponse, meResponse] = await Promise.all([
|
||||
api.get<User[]>("/users"),
|
||||
api.get<CurrentUser>("/me"),
|
||||
]);
|
||||
setUsers(usersResponse.data);
|
||||
setCurrentUser(meResponse.data);
|
||||
|
||||
if (hasPermission(meResponse.data, "roles.read")) {
|
||||
const rolesResponse = await api.get<Role[]>("/roles");
|
||||
setRoles(rolesResponse.data);
|
||||
} else {
|
||||
const userRoles = usersResponse.data.map((user) => user.primary_role);
|
||||
setRoles(userRoles.filter((role, index, all) => (
|
||||
all.findIndex((item) => item.id === role.id) === index
|
||||
)));
|
||||
}
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err));
|
||||
} finally {
|
||||
|
|
@ -77,10 +95,11 @@ export default function UsersPage() {
|
|||
user.last_name,
|
||||
user.username,
|
||||
user.email,
|
||||
user.role,
|
||||
user.primary_role.display_name,
|
||||
user.primary_role.name,
|
||||
].some((value) => value.toLowerCase().includes(term));
|
||||
})
|
||||
.filter((user) => roleFilter === "all" || user.role === roleFilter)
|
||||
.filter((user) => roleFilter === "all" || user.primary_role.name === roleFilter)
|
||||
.filter((user) => {
|
||||
if (statusFilter === "all") {
|
||||
return true;
|
||||
|
|
@ -182,10 +201,12 @@ export default function UsersPage() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<Button type="button" onClick={openCreateDialog}>
|
||||
<Plus size={16} />
|
||||
Neuer Benutzer
|
||||
</Button>
|
||||
{hasAllPermissions(currentUser, ["users.create", "roles.assign"]) && (
|
||||
<Button type="button" onClick={openCreateDialog}>
|
||||
<Plus size={16} />
|
||||
Neuer Benutzer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 rounded-lg border bg-white p-4 lg:flex-row lg:items-center">
|
||||
|
|
@ -207,9 +228,11 @@ export default function UsersPage() {
|
|||
className="h-8 rounded-lg border border-input bg-transparent px-2.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
>
|
||||
<option value="all">Alle Rollen</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="manager">Manager</option>
|
||||
<option value="user">Benutzer</option>
|
||||
{roles.map((role) => (
|
||||
<option key={role.id} value={role.name}>
|
||||
{role.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
|
|
@ -232,6 +255,8 @@ export default function UsersPage() {
|
|||
error={error}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
canUpdate={hasPermission(currentUser, "users.update")}
|
||||
canDelete={hasPermission(currentUser, "users.delete")}
|
||||
onSort={handleSort}
|
||||
onEdit={openEditDialog}
|
||||
onDelete={setDeleteUser}
|
||||
|
|
@ -266,6 +291,7 @@ export default function UsersPage() {
|
|||
user={selectedUser}
|
||||
pending={pending}
|
||||
serverError={formError}
|
||||
roles={roles}
|
||||
onOpenChange={setFormOpen}
|
||||
onSubmit={saveUser}
|
||||
/>
|
||||
|
|
@ -295,7 +321,7 @@ export default function UsersPage() {
|
|||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-slate-500">Rolle</dt>
|
||||
<dd className="font-medium">{deleteUser.role}</dd>
|
||||
<dd className="font-medium">{deleteUser.primary_role.display_name}</dd>
|
||||
</div>
|
||||
{deleteError && <p className="text-red-600">{deleteError}</p>}
|
||||
</dl>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue