Fixed creation of users + better frontend handling of permissions

This commit is contained in:
cdricms
2025-03-06 17:34:52 +01:00
parent 3c6038bce1
commit 7cb633b4c6
46 changed files with 1511 additions and 909 deletions

View File

@@ -48,9 +48,9 @@ export default function BlogTable({ user }: { user: IUser }) {
const { replace } = useRouter();
const canUpdate = hasPermissions(user.roles, { blogs: ["update"] });
const canInsert = hasPermissions(user.roles, { blogs: ["insert"] });
const canDelete = hasPermissions(user.roles, { blogs: ["delete"] });
const { blogs: blogsPerm } = hasPermissions(user.roles, {
blogs: ["update", "insert", "delete"],
} as const);
const updateSearchParam = useCallback(
(key: string, value?: string) => {
@@ -136,7 +136,7 @@ export default function BlogTable({ user }: { user: IUser }) {
</Button>
)}
</div>
{canInsert && (
{blogsPerm.insert && (
<Button asChild>
<Link href="/dashboard/blogs/new">Nouvel article</Link>
</Button>
@@ -151,7 +151,7 @@ export default function BlogTable({ user }: { user: IUser }) {
<TableHead>Catégorie</TableHead>
<TableHead>Auteur</TableHead>
<TableHead>Publié</TableHead>
{(canUpdate || canDelete) && (
{(blogsPerm.update || blogsPerm.delete) && (
<TableHead className="w-[100px]">
Actions
</TableHead>
@@ -175,7 +175,7 @@ export default function BlogTable({ user }: { user: IUser }) {
"MMM d, yyyy",
)}
</TableCell>
{(canDelete || canUpdate) && (
{(blogsPerm.delete || blogsPerm.update) && (
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -190,7 +190,7 @@ export default function BlogTable({ user }: { user: IUser }) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{canUpdate && (
{blogsPerm.update && (
<DropdownMenuItem asChild>
<Link
href={`/dashboard/blogs/${blog.blogID}`}
@@ -201,7 +201,7 @@ export default function BlogTable({ user }: { user: IUser }) {
</Link>
</DropdownMenuItem>
)}
{canDelete && (
{blogsPerm.delete && (
<DropdownMenuItem
className="flex items-center text-destructive focus:text-destructive"
onClick={() =>

View File

@@ -0,0 +1,16 @@
"use client";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
const BlogEditor = dynamic(
() => import("@/components/article/edit").then((mod) => mod.default),
{
ssr: false,
loading: () => <Loader2 className="animate-spin" />,
},
);
export default function NewBlog() {
return <BlogEditor />;
}

View File

@@ -1,16 +1,21 @@
"use client";
"use server";
import getMe from "@/lib/getMe";
import hasPermissions from "@/lib/hasPermissions";
import { redirect } from "next/navigation";
import NewBlog from "./_new";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
export default async function Page() {
const me = await getMe();
if (
!me ||
me.status === "Error" ||
!me.data ||
!hasPermissions(me.data.roles, {
blogs: ["insert"],
} as const).all
) {
redirect("/dashboard");
}
const BlogEditor = dynamic(
() => import("@/components/article/edit").then((mod) => mod.default),
{
ssr: false,
loading: () => <Loader2 className="animate-spin" />,
},
);
export default function Page() {
return <BlogEditor />;
return <NewBlog />;
}

View File

@@ -11,8 +11,8 @@ export default async function Page() {
me.status === "Error" ||
!me.data ||
!hasPermissions(me.data.roles, {
blogs: ["get"],
})
blogs: ["get"] as const,
}).all
) {
redirect("/dashboard");
}

View File

@@ -1,238 +0,0 @@
"use client";
import { UserIcon, Building, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Role, User } from "@/types/types";
import { useParams } from "next/navigation";
import { useApi } from "@/hooks/use-api";
import { useState } from "react";
import request from "@/lib/request";
import IUser from "@/interfaces/IUser";
import hasPermissions from "@/lib/hasPermissions";
export default function UserDetailsPage({ user }: { user: IUser }) {
const { uuid } = useParams<{ uuid: string }>();
const _user = useApi<User>(`/users/${uuid}`, {}, true);
const availableRoles = useApi<Role[]>("/roles", {}, true);
availableRoles.data ??= [];
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
// const [selectedOrg, setSelectedOrg] = useState("");
const addRole = async (role: Role) => {
const res = await request(
`/users/${_user.data?.userId}/roles/${role.id}/add`,
{ method: "PATCH", requiresAuth: true },
);
if (res.status === "Success") {
setSelectedRole(null);
_user.mutate();
}
};
const removeRole = async (role: Role) => {
const res = await request(
`/users/${_user.data?.userId}/roles/${role.id}/remove`,
{ method: "PATCH", requiresAuth: true },
);
if (res.status === "Success") _user.mutate();
};
const addOrganization = () => {
// if (selectedOrg && !user.organizations.includes(selectedOrg)) {
// setUser((prevUser) => ({
// ...prevUser,
// organizations: [...prevUser.organizations, selectedOrg],
// }));
// setSelectedOrg("");
// }
};
const removeOrganization = (orgToRemove: string) => {
// setUser((prevUser) => ({
// ...prevUser,
// organizations: prevUser.organizations.filter(
// (org) => org !== orgToRemove,
// ),
// }));
};
if (!_user.data || !_user.success) return <p>Error</p>;
return (
<div className="container mx-auto py-10">
<Card>
<CardContent className="pt-6">
<div className="grid gap-6">
<div className="flex items-center space-x-4">
<UserIcon className="h-12 w-12 text-gray-400" />
<div>
<h2 className="text-xl font-semibold">
{_user.data.firstname} {_user.data.lastname}
</h2>
<p className="text-sm text-gray-500">
{_user.data.email}
</p>
</div>
</div>
<div className="grid gap-6 md:grid-cols-2">
<div>
<h3 className="text-lg font-semibold mb-2">
Rôles
</h3>
<div className="flex flex-wrap gap-2">
{_user.data.roles?.map((role) => (
<Badge
key={role.id}
variant="secondary"
className="text-sm py-1 px-2"
>
{role.name}
{hasPermissions(user.roles, {
users: ["update"],
}) && (
<button
onClick={() =>
removeRole(role)
}
className="ml-2 text-gray-500 hover:text-gray-700"
>
<X className="h-3 w-3" />
</button>
)}
</Badge>
))}
</div>
{hasPermissions(user.roles, {
users: ["update"],
}) && (
<div className="mt-2 flex space-x-2">
<Select
value={
selectedRole
? selectedRole.name
: ""
}
onValueChange={(s) => {
const r =
availableRoles.data?.find(
(r) => r.name === s,
);
console.log(r);
if (r) setSelectedRole(r);
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sélectionner un rôle" />
</SelectTrigger>
<SelectContent>
{availableRoles.data
.filter(
(org) =>
!_user.data?.roles?.includes(
org,
),
)
.map((role) => (
<SelectItem
key={role.id}
value={role.name}
>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
disabled={
!_user.data || !selectedRole
}
onClick={() =>
addRole(selectedRole!)
}
className="flex items-center"
>
<Building className="mr-2 h-4 w-4" />
Ajouter le rôle
</Button>
</div>
)}
</div>
{/*<div>
<h3 className="text-lg font-semibold mb-2">
Organizations
</h3>
<div className="flex flex-wrap gap-2">
{user.data.organizations.map((org) => (
<Badge
key={org}
variant="outline"
className="text-sm py-1 px-2"
>
{org}
<button
onClick={() =>
removeOrganization(org)
}
className="ml-2 text-gray-500 hover:text-gray-700"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<div className="mt-2 flex space-x-2">
<Select
value={selectedOrg}
onValueChange={setSelectedOrg}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select an organization" />
</SelectTrigger>
<SelectContent>
{availableOrganizations
.filter(
(org) =>
!user.organizations.includes(
org,
),
)
.map((org) => (
<SelectItem
key={org}
value={org}
>
{org}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
onClick={addOrganization}
className="flex items-center"
>
<Building className="mr-2 h-4 w-4" />
Add Org
</Button>
</div>
</div> */}
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,20 +0,0 @@
import getMe from "@/lib/getMe";
import hasPermissions from "@/lib/hasPermissions";
import { redirect } from "next/navigation";
import UserDetailsPage from "./_user";
export default async function Page() {
const me = await getMe();
if (
!me ||
me.status === "Error" ||
!me.data ||
!hasPermissions(me.data.roles, {
users: ["get"],
})
) {
redirect("/dashboard");
}
return <UserDetailsPage user={me.data} />;
}

View File

@@ -12,7 +12,7 @@ export default async function Page({}) {
!me.data ||
!hasPermissions(me.data.roles, {
users: ["get"],
})
} as const).all
) {
redirect("/dashboard");
}

View File

@@ -19,9 +19,11 @@ export default function PlanningPage({ user }: { user: IUser }) {
if (success)
return (
<Planning
modifiable={hasPermissions(user.roles, {
events: ["update", "insert", "delete"],
})}
modifiable={
hasPermissions(user.roles, {
events: ["update", "insert", "delete"],
} as const).all
}
events={requestedEvents ?? []}
mutate={mutate}
/>

View File

@@ -12,7 +12,7 @@ export default async function Page() {
!me.data ||
!hasPermissions(me.data.roles, {
events: ["get"],
})
} as const).all
) {
redirect("/dashboard");
}

View File

@@ -11,7 +11,7 @@ export default async function Page() {
!me ||
me.status === "Error" ||
!me.data ||
!hasPermissions(me.data.roles, { media: ["get"] })
!hasPermissions(me.data.roles, { media: ["get"] } as const).all
) {
redirect("/dashboard");
}

View File

@@ -41,6 +41,10 @@ export default function RolesAndPermissions({ user }: { user: IUser }) {
const [newRoleName, setNewRoleName] = useState<string>("");
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
const { roles: rolesPerm } = hasPermissions(user.roles, {
roles: ["insert"],
} as const);
const { data: permissions } = useApi<PermissionsGrouped>(
"/permissions/grouped",
{},
@@ -80,7 +84,7 @@ export default function RolesAndPermissions({ user }: { user: IUser }) {
<div className="container mx-auto p-4 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">Rôles et Permissions</h1>
{hasPermissions(user.roles, { roles: ["insert"] }) && (
{rolesPerm.insert && (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button>
@@ -135,14 +139,17 @@ interface RoleCardProps {
}
function RoleCard({ role, onDelete, permissions, user }: RoleCardProps) {
const { roles, permissions: permPerms } = hasPermissions(user.roles, {
roles: ["delete", "update"],
permissions: ["update"],
} as const);
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>{toTitleCase(role.name)}</CardTitle>
<Button
disabled={
!hasPermissions(user.roles, { roles: ["delete"] })
}
disabled={!roles.delete}
variant="destructive"
size="icon"
onClick={onDelete}
@@ -155,10 +162,11 @@ function RoleCard({ role, onDelete, permissions, user }: RoleCardProps) {
return (
<ResourceSection
disabled={
!hasPermissions(user.roles, {
permissions: ["update"],
roles: ["update"],
})
!(roles.update && permPerms.update)
// !hasPermissions(user.roles, {
// permissions: ["update"],
// roles: ["update"],
// })
}
key={res}
resource={res}

View File

@@ -12,7 +12,7 @@ export default async function Page() {
!hasPermissions(me.data.roles, {
roles: ["get"],
permissions: ["get"],
})
} as const).all
) {
redirect("/dashboard");
}

View File

@@ -11,7 +11,7 @@ export default async function Page() {
!me.data ||
!hasPermissions(me.data.roles, {
shortcodes: ["get"],
})
} as const).all
) {
redirect("/dashboard");
}