Merge branch 'dev/cedric' into dev/guerby
This commit is contained in:
238
frontend/app/(auth)/dashboard/members/[uuid]/_user.tsx
Normal file
238
frontend/app/(auth)/dashboard/members/[uuid]/_user.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
"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,221 +1,20 @@
|
||||
"use client";
|
||||
import getMe from "@/lib/getMe";
|
||||
import hasPermissions from "@/lib/hasPermissions";
|
||||
import { redirect } from "next/navigation";
|
||||
import UserDetailsPage from "./_user";
|
||||
|
||||
import { UserIcon, Building, X } from "lucide-react";
|
||||
export default async function Page() {
|
||||
const me = await getMe();
|
||||
if (
|
||||
!me ||
|
||||
me.status === "Error" ||
|
||||
!me.data ||
|
||||
!hasPermissions(me.data.roles, {
|
||||
users: ["get"],
|
||||
})
|
||||
) {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
export default function UserDetailsPage() {
|
||||
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}
|
||||
<button
|
||||
onClick={() => removeRole(role)}
|
||||
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={
|
||||
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>
|
||||
);
|
||||
return <UserDetailsPage user={me.data} />;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
"use server";
|
||||
import MembersTable from "@/components/members-table";
|
||||
import getMe from "@/lib/getMe";
|
||||
import hasPermissions from "@/lib/hasPermissions";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
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 (
|
||||
<div className="container mx-auto px-4 py-10">
|
||||
<MembersTable />
|
||||
<MembersTable user={me.data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
29
frontend/app/(auth)/dashboard/planning/_planning.tsx
Normal file
29
frontend/app/(auth)/dashboard/planning/_planning.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import Planning from "@/components/planning";
|
||||
import { useApi } from "@/hooks/use-api";
|
||||
import ICalendarEvent from "@/interfaces/ICalendarEvent";
|
||||
import IUser from "@/interfaces/IUser";
|
||||
import hasPermissions from "@/lib/hasPermissions";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export default function PlanningPage({ user }: { user: IUser }) {
|
||||
const {
|
||||
data: requestedEvents,
|
||||
isLoading,
|
||||
success,
|
||||
mutate,
|
||||
} = useApi<ICalendarEvent[]>("/events", undefined, false, false);
|
||||
|
||||
if (isLoading) return <Loader2 className="animate-spin" />;
|
||||
if (success)
|
||||
return (
|
||||
<Planning
|
||||
modifiable={hasPermissions(user.roles, {
|
||||
events: ["update", "insert", "delete"],
|
||||
})}
|
||||
events={requestedEvents ?? []}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,21 @@
|
||||
"use client";
|
||||
"use server";
|
||||
import getMe from "@/lib/getMe";
|
||||
import hasPermissions from "@/lib/hasPermissions";
|
||||
import { redirect } from "next/navigation";
|
||||
import PlanningPage from "./_planning";
|
||||
|
||||
import Planning from "@/components/planning";
|
||||
import { useApi } from "@/hooks/use-api";
|
||||
import ICalendarEvent from "@/interfaces/ICalendarEvent";
|
||||
import { Loader2 } from "lucide-react";
|
||||
export default async function Page() {
|
||||
const me = await getMe();
|
||||
if (
|
||||
!me ||
|
||||
me.status === "Error" ||
|
||||
!me.data ||
|
||||
!hasPermissions(me.data.roles, {
|
||||
events: ["get"],
|
||||
})
|
||||
) {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const {
|
||||
data: requestedEvents,
|
||||
isLoading,
|
||||
success,
|
||||
mutate,
|
||||
} = useApi<ICalendarEvent[]>("/events", undefined, false, false);
|
||||
|
||||
if (isLoading) return <Loader2 className="animate-spin" />;
|
||||
if (success)
|
||||
return (
|
||||
<Planning
|
||||
modifiable
|
||||
events={requestedEvents ?? []}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
return <PlanningPage user={me.data} />;
|
||||
}
|
||||
|
||||
169
frontend/app/(auth)/dashboard/settings/media/_media.tsx
Normal file
169
frontend/app/(auth)/dashboard/settings/media/_media.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import { PhotoDialog } from "@/components/photo-dialog";
|
||||
import useFileUpload from "@/hooks/use-file-upload";
|
||||
import useMedia from "@/hooks/use-media";
|
||||
import Media from "@/interfaces/Media";
|
||||
import request from "@/lib/request";
|
||||
import IUser from "@/interfaces/IUser";
|
||||
|
||||
export default function PhotoGallery({ user }: { user: IUser }) {
|
||||
const {
|
||||
data,
|
||||
error: mediaError,
|
||||
isLoading,
|
||||
success,
|
||||
setPage,
|
||||
setLimit,
|
||||
mutate,
|
||||
} = useMedia();
|
||||
console.log(data);
|
||||
const [selectedPhoto, setSelectedPhoto] = useState<Media | null>(null);
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||
const { progress, isUploading, error, uploadFile, cancelUpload } =
|
||||
useFileUpload();
|
||||
|
||||
const handleAddPhoto = (newPhoto: Omit<Media, "id">, file: File) => {
|
||||
uploadFile(file, "/media/upload", (response) => {
|
||||
mutate();
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdatePhoto = async (
|
||||
body: Media | Omit<Media, "id">,
|
||||
file: File,
|
||||
) => {
|
||||
if (selectedPhoto) {
|
||||
const res = await request<Media>(
|
||||
`/media/${selectedPhoto.id}/update`,
|
||||
{
|
||||
method: "PATCH",
|
||||
requiresAuth: true,
|
||||
body,
|
||||
},
|
||||
);
|
||||
if (res.status === "Success") {
|
||||
mutate();
|
||||
}
|
||||
}
|
||||
setSelectedPhoto(null);
|
||||
};
|
||||
|
||||
const handleDeletePhoto = async (id: Media["id"]) => {
|
||||
try {
|
||||
const res = await request<undefined>(`/media/${id}/delete`, {
|
||||
method: "DELETE",
|
||||
requiresAuth: true,
|
||||
});
|
||||
if (res.status === "Success") mutate();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
setSelectedPhoto(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">Gallerie Photo</h1>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
onClick={() => setIsAddDialogOpen(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" /> Ajouter une photo
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{data?.items.map((photo) => (
|
||||
<div
|
||||
key={photo.id}
|
||||
className="aspect-square overflow-hidden rounded-lg shadow-md cursor-pointer"
|
||||
onClick={() => setSelectedPhoto(photo)}
|
||||
>
|
||||
<Image
|
||||
src={photo.url || "/placeholder.svg"}
|
||||
alt={photo.alt}
|
||||
width={300}
|
||||
height={300}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Pagination className="mt-8">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage((prev) => Math.max(prev - 1, 1));
|
||||
}}
|
||||
className={
|
||||
data?.page === 1
|
||||
? "pointer-events-none opacity-50"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
{[...Array(data?.totalPages)].map((_, i) => (
|
||||
<PaginationItem key={i + 1}>
|
||||
<PaginationLink
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage(i + 1);
|
||||
}}
|
||||
isActive={data?.page === i + 1}
|
||||
>
|
||||
{i + 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage((prev) =>
|
||||
Math.min(prev + 1, data?.totalPages ?? 1),
|
||||
);
|
||||
}}
|
||||
className={
|
||||
data?.page === data?.totalPages
|
||||
? "pointer-events-none opacity-50"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
<PhotoDialog
|
||||
isOpen={!!selectedPhoto}
|
||||
photo={selectedPhoto || undefined}
|
||||
onClose={() =>
|
||||
setSelectedPhoto((p) => (isUploading ? p : null))
|
||||
}
|
||||
onDelete={handleDeletePhoto}
|
||||
onSave={handleUpdatePhoto}
|
||||
/>
|
||||
<PhotoDialog
|
||||
isOpen={isAddDialogOpen}
|
||||
onClose={() => setIsAddDialogOpen(false)}
|
||||
onSave={handleAddPhoto}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,168 +1,20 @@
|
||||
"use client";
|
||||
"use server";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import { PhotoDialog } from "@/components/photo-dialog";
|
||||
import useFileUpload from "@/hooks/use-file-upload";
|
||||
import useMedia from "@/hooks/use-media";
|
||||
import Media from "@/interfaces/Media";
|
||||
import request from "@/lib/request";
|
||||
import getMe from "@/lib/getMe";
|
||||
import { redirect } from "next/navigation";
|
||||
import PhotoGallery from "./_media";
|
||||
import hasPermissions from "@/lib/hasPermissions";
|
||||
|
||||
export default function PhotoGallery() {
|
||||
const {
|
||||
data,
|
||||
error: mediaError,
|
||||
isLoading,
|
||||
success,
|
||||
setPage,
|
||||
setLimit,
|
||||
mutate,
|
||||
} = useMedia();
|
||||
console.log(data);
|
||||
const [selectedPhoto, setSelectedPhoto] = useState<Media | null>(null);
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||
const { progress, isUploading, error, uploadFile, cancelUpload } =
|
||||
useFileUpload();
|
||||
export default async function Page() {
|
||||
const me = await getMe();
|
||||
if (
|
||||
!me ||
|
||||
me.status === "Error" ||
|
||||
!me.data ||
|
||||
!hasPermissions(me.data.roles, { media: ["get"] })
|
||||
) {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
const handleAddPhoto = (newPhoto: Omit<Media, "id">, file: File) => {
|
||||
uploadFile(file, "/media/upload", (response) => {
|
||||
mutate();
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdatePhoto = async (
|
||||
body: Media | Omit<Media, "id">,
|
||||
file: File,
|
||||
) => {
|
||||
if (selectedPhoto) {
|
||||
const res = await request<Media>(
|
||||
`/media/${selectedPhoto.id}/update`,
|
||||
{
|
||||
method: "PATCH",
|
||||
requiresAuth: true,
|
||||
body,
|
||||
},
|
||||
);
|
||||
if (res.status === "Success") {
|
||||
mutate();
|
||||
}
|
||||
}
|
||||
setSelectedPhoto(null);
|
||||
};
|
||||
|
||||
const handleDeletePhoto = async (id: Media["id"]) => {
|
||||
try {
|
||||
const res = await request<undefined>(`/media/${id}/delete`, {
|
||||
method: "DELETE",
|
||||
requiresAuth: true,
|
||||
});
|
||||
if (res.status === "Success") mutate();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
setSelectedPhoto(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">Gallerie Photo</h1>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
onClick={() => setIsAddDialogOpen(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" /> Ajouter une photo
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{data?.items.map((photo) => (
|
||||
<div
|
||||
key={photo.id}
|
||||
className="aspect-square overflow-hidden rounded-lg shadow-md cursor-pointer"
|
||||
onClick={() => setSelectedPhoto(photo)}
|
||||
>
|
||||
<Image
|
||||
src={photo.url || "/placeholder.svg"}
|
||||
alt={photo.alt}
|
||||
width={300}
|
||||
height={300}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Pagination className="mt-8">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage((prev) => Math.max(prev - 1, 1));
|
||||
}}
|
||||
className={
|
||||
data?.page === 1
|
||||
? "pointer-events-none opacity-50"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
{[...Array(data?.totalPages)].map((_, i) => (
|
||||
<PaginationItem key={i + 1}>
|
||||
<PaginationLink
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage(i + 1);
|
||||
}}
|
||||
isActive={data?.page === i + 1}
|
||||
>
|
||||
{i + 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage((prev) =>
|
||||
Math.min(prev + 1, data?.totalPages ?? 1),
|
||||
);
|
||||
}}
|
||||
className={
|
||||
data?.page === data?.totalPages
|
||||
? "pointer-events-none opacity-50"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
<PhotoDialog
|
||||
isOpen={!!selectedPhoto}
|
||||
photo={selectedPhoto || undefined}
|
||||
onClose={() =>
|
||||
setSelectedPhoto((p) => (isUploading ? p : null))
|
||||
}
|
||||
onDelete={handleDeletePhoto}
|
||||
onSave={handleUpdatePhoto}
|
||||
/>
|
||||
<PhotoDialog
|
||||
isOpen={isAddDialogOpen}
|
||||
onClose={() => setIsAddDialogOpen(false)}
|
||||
onSave={handleAddPhoto}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return <PhotoGallery user={me.data} />;
|
||||
}
|
||||
|
||||
247
frontend/app/(auth)/dashboard/settings/roles/_roles.tsx
Normal file
247
frontend/app/(auth)/dashboard/settings/roles/_roles.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ChevronDown, ChevronRight, Plus, Trash2 } from "lucide-react";
|
||||
import { toTitleCase } from "@/lib/utils";
|
||||
import { useApi } from "@/hooks/use-api";
|
||||
import request from "@/lib/request";
|
||||
import IUser from "@/interfaces/IUser";
|
||||
import hasPermissions from "@/lib/hasPermissions";
|
||||
|
||||
type Action = string;
|
||||
|
||||
interface Permission {
|
||||
resource: string;
|
||||
action: Action;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions?: Permission[];
|
||||
}
|
||||
|
||||
interface PermissionsGrouped {
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
export default function RolesAndPermissions({ user }: { user: IUser }) {
|
||||
const [newRoleName, setNewRoleName] = useState<string>("");
|
||||
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const { data: permissions } = useApi<PermissionsGrouped>(
|
||||
"/permissions/grouped",
|
||||
{},
|
||||
true,
|
||||
);
|
||||
|
||||
const { data: roles, mutate: rolesMutate } = useApi<Role[]>(
|
||||
"/roles",
|
||||
{},
|
||||
true,
|
||||
);
|
||||
|
||||
const addNewRole = async () => {
|
||||
if (newRoleName.trim() === "") return;
|
||||
|
||||
const res = await request("/roles/new", {
|
||||
requiresAuth: true,
|
||||
method: "POST",
|
||||
body: { name: newRoleName },
|
||||
});
|
||||
|
||||
if (res.status === "Success") rolesMutate();
|
||||
|
||||
setNewRoleName("");
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const deleteRole = async (id: string) => {
|
||||
const res = await request(`/roles/${id}/delete`, {
|
||||
method: "DELETE",
|
||||
requiresAuth: true,
|
||||
});
|
||||
if (res.status === "Success") rolesMutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<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"] }) && (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" /> Nouveau rôle
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nouveau rôle</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Input
|
||||
placeholder="Nom du rôle"
|
||||
value={newRoleName}
|
||||
onChange={(e) =>
|
||||
setNewRoleName(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={addNewRole}
|
||||
disabled={newRoleName.trim() === ""}
|
||||
>
|
||||
Ajouter
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
{permissions &&
|
||||
roles &&
|
||||
roles.map((role, index) => (
|
||||
<RoleCard
|
||||
user={user}
|
||||
key={index}
|
||||
role={role}
|
||||
permissions={permissions}
|
||||
onDelete={() => deleteRole(role.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RoleCardProps {
|
||||
user: IUser;
|
||||
role: Role;
|
||||
onDelete: () => void;
|
||||
permissions: PermissionsGrouped;
|
||||
}
|
||||
|
||||
function RoleCard({ role, onDelete, permissions, user }: RoleCardProps) {
|
||||
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"] })
|
||||
}
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{Object.entries(permissions).map(([res, actions]) => {
|
||||
return (
|
||||
<ResourceSection
|
||||
disabled={
|
||||
!hasPermissions(user.roles, {
|
||||
permissions: ["update"],
|
||||
roles: ["update"],
|
||||
})
|
||||
}
|
||||
key={res}
|
||||
resource={res}
|
||||
defaultActions={actions}
|
||||
role={role}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface ResourceSectionProps {
|
||||
disabled?: boolean;
|
||||
resource: string;
|
||||
defaultActions: string[];
|
||||
role: Role;
|
||||
}
|
||||
|
||||
function ResourceSection({
|
||||
resource,
|
||||
defaultActions,
|
||||
role,
|
||||
disabled = false,
|
||||
}: ResourceSectionProps) {
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||
|
||||
const a = (role.permissions ?? [])
|
||||
.map((p) => (p.resource === resource ? p.action : null))
|
||||
.filter((a) => a !== null);
|
||||
|
||||
const ActionCheckbox = ({ action }: { action: Action }) => {
|
||||
const [checked, setChecked] = useState(a.includes(action));
|
||||
return (
|
||||
<div key={action} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
disabled={disabled}
|
||||
onCheckedChange={async (e) => {
|
||||
if (typeof e === "boolean") {
|
||||
const res = await request(
|
||||
`/roles/${role.id}/permissions/${resource}/${action}/${e ? "add" : "remove"}`,
|
||||
{ method: "PATCH", requiresAuth: true },
|
||||
);
|
||||
if (res.status === "Success") setChecked(e);
|
||||
}
|
||||
}}
|
||||
checked={checked}
|
||||
id={`${resource}-${action}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${resource}-${action}`}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{action}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
className="flex items-center text-lg font-semibold mb-2"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="mr-2" />
|
||||
) : (
|
||||
<ChevronRight className="mr-2" />
|
||||
)}
|
||||
{toTitleCase(resource)}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 ml-6">
|
||||
{defaultActions.map((action) => (
|
||||
<ActionCheckbox
|
||||
key={`${resource}:${action}`}
|
||||
action={action}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,223 +1,21 @@
|
||||
"use client";
|
||||
import getMe from "@/lib/getMe";
|
||||
import hasPermissions from "@/lib/hasPermissions";
|
||||
import { redirect } from "next/navigation";
|
||||
import RolesAndPermissions from "./_roles";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ChevronDown, ChevronRight, Plus, Trash2 } from "lucide-react";
|
||||
import { toTitleCase } from "@/lib/utils";
|
||||
import { useApi } from "@/hooks/use-api";
|
||||
import request from "@/lib/request";
|
||||
export default async function Page() {
|
||||
const me = await getMe();
|
||||
if (
|
||||
!me ||
|
||||
me.status === "Error" ||
|
||||
!me.data ||
|
||||
!hasPermissions(me.data.roles, {
|
||||
roles: ["get"],
|
||||
permissions: ["get"],
|
||||
})
|
||||
) {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
type Action = string;
|
||||
|
||||
interface Permission {
|
||||
resource: string;
|
||||
action: Action;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions?: Permission[];
|
||||
}
|
||||
|
||||
interface PermissionsGrouped {
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
export default function RolesAndPermissions() {
|
||||
const [newRoleName, setNewRoleName] = useState<string>("");
|
||||
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const { data: permissions } = useApi<PermissionsGrouped>(
|
||||
"/permissions/grouped",
|
||||
{},
|
||||
true,
|
||||
);
|
||||
|
||||
const { data: roles, mutate: rolesMutate } = useApi<Role[]>(
|
||||
"/roles",
|
||||
{},
|
||||
true,
|
||||
);
|
||||
|
||||
const addNewRole = async () => {
|
||||
if (newRoleName.trim() === "") return;
|
||||
|
||||
const res = await request("/roles/new", {
|
||||
requiresAuth: true,
|
||||
method: "POST",
|
||||
body: { name: newRoleName },
|
||||
});
|
||||
|
||||
if (res.status === "Success") rolesMutate();
|
||||
|
||||
setNewRoleName("");
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
|
||||
const deleteRole = async (id: string) => {
|
||||
const res = await request(`/roles/${id}/delete`, {
|
||||
method: "DELETE",
|
||||
requiresAuth: true,
|
||||
});
|
||||
if (res.status === "Success") rolesMutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" /> Nouveau rôle
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nouveau rôle</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Input
|
||||
placeholder="Nom du rôle"
|
||||
value={newRoleName}
|
||||
onChange={(e) => setNewRoleName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={addNewRole}
|
||||
disabled={newRoleName.trim() === ""}
|
||||
>
|
||||
Ajouter
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
{permissions &&
|
||||
roles &&
|
||||
roles.map((role, index) => (
|
||||
<RoleCard
|
||||
key={index}
|
||||
role={role}
|
||||
permissions={permissions}
|
||||
onDelete={() => deleteRole(role.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RoleCardProps {
|
||||
role: Role;
|
||||
onDelete: () => void;
|
||||
permissions: PermissionsGrouped;
|
||||
}
|
||||
|
||||
function RoleCard({ role, onDelete, permissions }: RoleCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle>{toTitleCase(role.name)}</CardTitle>
|
||||
<Button variant="destructive" size="icon" onClick={onDelete}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{Object.entries(permissions).map(([res, actions]) => {
|
||||
return (
|
||||
<ResourceSection
|
||||
key={res}
|
||||
resource={res}
|
||||
defaultActions={actions}
|
||||
role={role}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface ResourceSectionProps {
|
||||
resource: string;
|
||||
defaultActions: string[];
|
||||
role: Role;
|
||||
}
|
||||
|
||||
function ResourceSection({
|
||||
resource,
|
||||
defaultActions,
|
||||
role,
|
||||
}: ResourceSectionProps) {
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||
|
||||
const a = (role.permissions ?? [])
|
||||
.map((p) => (p.resource === resource ? p.action : null))
|
||||
.filter((a) => a !== null);
|
||||
|
||||
const ActionCheckbox = ({ action }: { action: Action }) => {
|
||||
const [checked, setChecked] = useState(a.includes(action));
|
||||
return (
|
||||
<div key={action} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
onCheckedChange={async (e) => {
|
||||
if (typeof e === "boolean") {
|
||||
const res = await request(
|
||||
`/roles/${role.id}/permissions/${resource}/${action}/${e ? "add" : "remove"}`,
|
||||
{ method: "PATCH", requiresAuth: true },
|
||||
);
|
||||
if (res.status === "Success") setChecked(e);
|
||||
}
|
||||
}}
|
||||
checked={checked}
|
||||
id={`${resource}-${action}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${resource}-${action}`}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{action}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
className="flex items-center text-lg font-semibold mb-2"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="mr-2" />
|
||||
) : (
|
||||
<ChevronRight className="mr-2" />
|
||||
)}
|
||||
{toTitleCase(resource)}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 ml-6">
|
||||
{defaultActions.map((action) => (
|
||||
<ActionCheckbox
|
||||
key={`${resource}:${action}`}
|
||||
action={action}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return <RolesAndPermissions user={me.data} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ShortcodeTable } from "@/components/shortcodes-table";
|
||||
import type IShortcode from "@/interfaces/IShortcode";
|
||||
import { useApi } from "@/hooks/use-api";
|
||||
import request from "@/lib/request";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import IUser from "@/interfaces/IUser";
|
||||
|
||||
export default function ShortcodesPage({ user }: { user: IUser }) {
|
||||
const {
|
||||
data: shortcodes,
|
||||
error,
|
||||
isLoading,
|
||||
mutate,
|
||||
success,
|
||||
} = useApi<IShortcode[]>("/shortcodes", undefined, true);
|
||||
|
||||
console.log(shortcodes);
|
||||
|
||||
const handleUpdate = async (updatedShortcode: IShortcode) => {
|
||||
const res = await request<IShortcode>(
|
||||
`/shortcodes/${updatedShortcode.code}/update`,
|
||||
{
|
||||
method: "PATCH",
|
||||
requiresAuth: true,
|
||||
body: updatedShortcode,
|
||||
},
|
||||
);
|
||||
mutate();
|
||||
// Implement update logic here
|
||||
console.log("Update shortcode:", updatedShortcode);
|
||||
};
|
||||
|
||||
const handleDelete = async (code: string) => {
|
||||
const res = await request<undefined>(`/shortcodes/${code}/delete`, {
|
||||
requiresAuth: true,
|
||||
method: "DELETE",
|
||||
});
|
||||
mutate();
|
||||
};
|
||||
|
||||
const handleAdd = async (newShortcode: Omit<IShortcode, "id">) => {
|
||||
const res = await request<IShortcode>(`/shortcodes/new`, {
|
||||
body: newShortcode,
|
||||
method: "POST",
|
||||
requiresAuth: true,
|
||||
});
|
||||
console.log(res);
|
||||
mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-10">
|
||||
<h1 className="text-2xl font-bold mb-5">Shortcodes</h1>
|
||||
{isLoading && (
|
||||
<Loader2 className="flex w-full min-w-0 flex-col gap-1 justify-center animate-spin" />
|
||||
)}
|
||||
{error && <p>{error}</p>}
|
||||
<ShortcodeTable
|
||||
user={user}
|
||||
shortcodes={shortcodes ?? []}
|
||||
onUpdate={handleUpdate}
|
||||
onDelete={handleDelete}
|
||||
onAdd={handleAdd}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +1,20 @@
|
||||
"use client";
|
||||
import getMe from "@/lib/getMe";
|
||||
import hasPermissions from "@/lib/hasPermissions";
|
||||
import { redirect } from "next/navigation";
|
||||
import ShortcodesPage from "./_shortcodes";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ShortcodeTable } from "@/components/shortcodes-table";
|
||||
import type IShortcode from "@/interfaces/IShortcode";
|
||||
import { useApi } from "@/hooks/use-api";
|
||||
import request from "@/lib/request";
|
||||
import { Loader2 } from "lucide-react";
|
||||
export default async function Page() {
|
||||
const me = await getMe();
|
||||
if (
|
||||
!me ||
|
||||
me.status === "Error" ||
|
||||
!me.data ||
|
||||
!hasPermissions(me.data.roles, {
|
||||
shortcodes: ["get"],
|
||||
})
|
||||
) {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
export default function ShortcodesPage() {
|
||||
const {
|
||||
data: shortcodes,
|
||||
error,
|
||||
isLoading,
|
||||
mutate,
|
||||
success,
|
||||
} = useApi<IShortcode[]>("/shortcodes", undefined, true);
|
||||
|
||||
console.log(shortcodes);
|
||||
|
||||
const handleUpdate = async (updatedShortcode: IShortcode) => {
|
||||
const res = await request<IShortcode>(
|
||||
`/shortcodes/${updatedShortcode.code}/update`,
|
||||
{
|
||||
method: "PATCH",
|
||||
requiresAuth: true,
|
||||
body: updatedShortcode,
|
||||
},
|
||||
);
|
||||
mutate();
|
||||
// Implement update logic here
|
||||
console.log("Update shortcode:", updatedShortcode);
|
||||
};
|
||||
|
||||
const handleDelete = async (code: string) => {
|
||||
const res = await request<undefined>(`/shortcodes/${code}/delete`, {
|
||||
requiresAuth: true,
|
||||
method: "DELETE",
|
||||
});
|
||||
mutate();
|
||||
};
|
||||
|
||||
const handleAdd = async (newShortcode: Omit<IShortcode, "id">) => {
|
||||
const res = await request<IShortcode>(`/shortcodes/new`, {
|
||||
body: newShortcode,
|
||||
method: "POST",
|
||||
requiresAuth: true,
|
||||
});
|
||||
console.log(res);
|
||||
mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-10">
|
||||
<h1 className="text-2xl font-bold mb-5">Shortcodes</h1>
|
||||
{isLoading && (
|
||||
<Loader2 className="flex w-full min-w-0 flex-col gap-1 justify-center animate-spin" />
|
||||
)}
|
||||
{error && <p>{error}</p>}
|
||||
<ShortcodeTable
|
||||
shortcodes={shortcodes ?? []}
|
||||
onUpdate={handleUpdate}
|
||||
onDelete={handleDelete}
|
||||
onAdd={handleAdd}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return <ShortcodesPage user={me.data} />;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import useLogin from "@/hooks/use-login";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
@@ -16,12 +16,20 @@ export function LoginForm({
|
||||
const [password, setPassword] = useState("");
|
||||
const { login, loading, isSuccess } = useLogin();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const res = await login({ email, password });
|
||||
if (res.status === "Success") router.push("/dashboard");
|
||||
if (res.status === "Success") {
|
||||
const redirectTo = searchParams.get("redirectTo");
|
||||
if (redirectTo) {
|
||||
router.push(redirectTo);
|
||||
} else {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
}
|
||||
console.log(res);
|
||||
} catch (err: any) {
|
||||
console.log(err.message);
|
||||
|
||||
@@ -23,8 +23,10 @@ import {
|
||||
UserRoundPlus,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import IUser from "@/interfaces/IUser";
|
||||
import hasPermissions from "@/lib/hasPermissions";
|
||||
|
||||
export default function MembersTable() {
|
||||
export default function MembersTable({ user }: { user: IUser }) {
|
||||
const {
|
||||
data: members,
|
||||
error,
|
||||
@@ -97,9 +99,11 @@ export default function MembersTable() {
|
||||
<Button onClick={toggleSelectMode}>
|
||||
{selectMode ? <CircleX /> : "Selectionner"}
|
||||
</Button>
|
||||
<Button onClick={() => handleOpenDialog(null)}>
|
||||
<UserRoundPlus />
|
||||
</Button>
|
||||
{hasPermissions(user.roles, { users: ["insert"] }) && (
|
||||
<Button onClick={() => handleOpenDialog(null)}>
|
||||
<UserRoundPlus />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<ScrollArea className="h-full rounded-md border">
|
||||
@@ -162,25 +166,35 @@ export default function MembersTable() {
|
||||
<TableCell>{member.phone}</TableCell>
|
||||
<TableCell>{member.role}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mr-2"
|
||||
onClick={() =>
|
||||
handleOpenDialog(member)
|
||||
}
|
||||
>
|
||||
<UserRoundPen />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleDelete(member.userId!)
|
||||
}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
{hasPermissions(user.roles, {
|
||||
users: ["update"],
|
||||
}) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mr-2"
|
||||
onClick={() =>
|
||||
handleOpenDialog(member)
|
||||
}
|
||||
>
|
||||
<UserRoundPen />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermissions(user.roles, {
|
||||
users: ["delete"],
|
||||
}) && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleDelete(
|
||||
member.userId!,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -75,7 +75,7 @@ const Planning: React.FC<{
|
||||
description: res.message,
|
||||
});
|
||||
} else {
|
||||
mutate?.();
|
||||
// mutate?.();
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error)
|
||||
|
||||
@@ -19,8 +19,11 @@ import { MoreHorizontal } from "lucide-react";
|
||||
import type IShortcode from "@/interfaces/IShortcode";
|
||||
import ShortcodeDialog from "@/components/shortcode-dialogue";
|
||||
import { useState } from "react";
|
||||
import IUser from "@/interfaces/IUser";
|
||||
import hasPermissions from "@/lib/hasPermissions";
|
||||
|
||||
interface ShortcodeTableProps {
|
||||
user: IUser;
|
||||
shortcodes: IShortcode[];
|
||||
onUpdate: (shortcode: IShortcode) => void;
|
||||
onDelete: (id: string) => void;
|
||||
@@ -32,6 +35,7 @@ export function ShortcodeTable({
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onAdd,
|
||||
user,
|
||||
}: ShortcodeTableProps) {
|
||||
const [shortcodeSelected, setUpdateDialog] = useState<IShortcode | null>(
|
||||
null,
|
||||
@@ -39,20 +43,22 @@ export function ShortcodeTable({
|
||||
const [addDialog, setAddDialog] = useState<boolean>(false);
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setAddDialog(true);
|
||||
}}
|
||||
>
|
||||
Ajouter
|
||||
</Button>
|
||||
<ShortcodeDialog
|
||||
onSave={onAdd}
|
||||
open={addDialog}
|
||||
setOpen={() => setAddDialog(false)}
|
||||
/>
|
||||
</div>
|
||||
{hasPermissions(user.roles, { shortcodes: ["insert"] }) && (
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setAddDialog(true);
|
||||
}}
|
||||
>
|
||||
Ajouter
|
||||
</Button>
|
||||
<ShortcodeDialog
|
||||
onSave={onAdd}
|
||||
open={addDialog}
|
||||
setOpen={() => setAddDialog(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -91,20 +97,30 @@ export function ShortcodeTable({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setUpdateDialog(shortcode)
|
||||
}
|
||||
>
|
||||
Mettre à jour
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
onDelete(shortcode.code)
|
||||
}
|
||||
>
|
||||
Supprimer
|
||||
</DropdownMenuItem>
|
||||
{hasPermissions(user.roles, {
|
||||
shortcodes: ["update"],
|
||||
}) && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setUpdateDialog(
|
||||
shortcode,
|
||||
)
|
||||
}
|
||||
>
|
||||
Mettre à jour
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{hasPermissions(user.roles, {
|
||||
shortcodes: ["delete"],
|
||||
}) && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
onDelete(shortcode.code)
|
||||
}
|
||||
>
|
||||
Supprimer
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
|
||||
17
frontend/hooks/use-roles.tsx
Normal file
17
frontend/hooks/use-roles.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import getMe from "@/lib/getMe";
|
||||
import { Role } from "@/types/types";
|
||||
import { getCookie } from "cookies-next";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function useRoles() {
|
||||
const [roles, setRoles] = useState<Role[] | null>(null);
|
||||
const cookie = getCookie("auth_token");
|
||||
useEffect(() => {
|
||||
if (cookie)
|
||||
getMe(cookie?.toString()).then((me) => {
|
||||
setRoles(me?.data?.roles ?? null);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return roles;
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Role } from "@/types/types";
|
||||
|
||||
export default interface IUser {
|
||||
userId: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: string;
|
||||
roles: Role[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
22
frontend/lib/getMe.ts
Normal file
22
frontend/lib/getMe.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { cache } from "react";
|
||||
import { API_URL } from "./constants";
|
||||
import { ApiResponse } from "@/types/types";
|
||||
import IUser from "@/interfaces/IUser";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const getMe = cache(
|
||||
async (sessionCookie?: string): Promise<ApiResponse<IUser> | null> => {
|
||||
if (!sessionCookie) {
|
||||
const store = await cookies();
|
||||
const token = store.get("auth_token")?.value;
|
||||
if (!token) return null;
|
||||
sessionCookie = token;
|
||||
}
|
||||
const res = await fetch(`${API_URL}/users/me`, {
|
||||
headers: { Authorization: `Bearer ${sessionCookie}` },
|
||||
});
|
||||
return await res.json();
|
||||
},
|
||||
);
|
||||
|
||||
export default getMe;
|
||||
25
frontend/lib/hasPermissions.ts
Normal file
25
frontend/lib/hasPermissions.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Role } from "@/types/types";
|
||||
|
||||
export default function hasPermissions(
|
||||
roles: Role[],
|
||||
permissions: { [key: string]: string[] },
|
||||
) {
|
||||
const permissionsSet: Map<string, null> = new Map();
|
||||
for (const role of roles) {
|
||||
if (!role.permissions) continue;
|
||||
for (const perm of role?.permissions) {
|
||||
const key = perm.resource + ":" + perm.action;
|
||||
permissionsSet.set(key, null);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [perm, actions] of Object.entries(permissions)) {
|
||||
for (const action of actions) {
|
||||
if (!permissionsSet.has(perm + ":" + action)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { ApiResponse } from "./types/types";
|
||||
import { API_URL } from "./lib/constants";
|
||||
import IUser from "./interfaces/IUser";
|
||||
import getMe from "./lib/getMe";
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const sessionCookie = request.cookies.get("auth_token")?.value;
|
||||
@@ -17,11 +16,8 @@ export async function middleware(request: NextRequest) {
|
||||
|
||||
try {
|
||||
console.log(API_URL);
|
||||
const res = await fetch(`${API_URL}/users/me`, {
|
||||
headers: { Authorization: `Bearer ${sessionCookie}` },
|
||||
});
|
||||
const js: ApiResponse<IUser> = await res.json();
|
||||
if (js.status === "Error") {
|
||||
const js = await getMe(sessionCookie);
|
||||
if (js?.status === "Error") {
|
||||
console.log(js.message);
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface Permission {
|
||||
export interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: Permission[];
|
||||
permissions?: Permission[];
|
||||
}
|
||||
|
||||
// Status type as a string literal
|
||||
|
||||
Reference in New Issue
Block a user