Fixed creation of users + better frontend handling of permissions
This commit is contained in:
@@ -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={() =>
|
||||
|
||||
16
frontend/app/(auth)/dashboard/blogs/new/_new.tsx
Normal file
16
frontend/app/(auth)/dashboard/blogs/new/_new.tsx
Normal 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 />;
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export default async function Page({}) {
|
||||
!me.data ||
|
||||
!hasPermissions(me.data.roles, {
|
||||
users: ["get"],
|
||||
})
|
||||
} as const).all
|
||||
) {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -12,7 +12,7 @@ export default async function Page() {
|
||||
!me.data ||
|
||||
!hasPermissions(me.data.roles, {
|
||||
events: ["get"],
|
||||
})
|
||||
} as const).all
|
||||
) {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default async function Page() {
|
||||
!hasPermissions(me.data.roles, {
|
||||
roles: ["get"],
|
||||
permissions: ["get"],
|
||||
})
|
||||
} as const).all
|
||||
) {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default async function Page() {
|
||||
!me.data ||
|
||||
!hasPermissions(me.data.roles, {
|
||||
shortcodes: ["get"],
|
||||
})
|
||||
} as const).all
|
||||
) {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ export default async function Home() {
|
||||
})}
|
||||
</Gallery>
|
||||
)}
|
||||
<Testimonial />
|
||||
{/*<Testimonial />*/}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -82,6 +83,7 @@ body {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@/lib/utils";
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "@/app/globals.css";
|
||||
|
||||
13
frontend/app/robots.ts
Normal file
13
frontend/app/robots.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { BASE_URL } from "@/lib/constants";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
disallow: ["/dashboard/", "/gallery/"],
|
||||
},
|
||||
sitemap: `${BASE_URL}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
43
frontend/app/sitemap.ts
Normal file
43
frontend/app/sitemap.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { BASE_URL } from "@/lib/constants";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{
|
||||
url: BASE_URL,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/about`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/blog`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly",
|
||||
priority: 0.5,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/planning`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/gallery`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${BASE_URL}/contact`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.8,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user