From 2011ae93b60801f5b96923c5521480e51caf1ecf Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:16:47 +0100 Subject: [PATCH 1/2] Availability based on permissions --- backend/api/blogs_routes.go | 5 +- backend/api/media_routes.go | 20 +- backend/api/permissions_routes.go | 20 +- backend/api/roles_routes.go | 40 +-- backend/api/shortcodes_routes.go | 20 +- backend/api/users/user.go | 2 +- .../(auth)/dashboard/members/[uuid]/_user.tsx | 238 +++++++++++++++++ .../(auth)/dashboard/members/[uuid]/page.tsx | 235 ++--------------- .../app/(auth)/dashboard/members/page.tsx | 16 +- .../(auth)/dashboard/planning/_planning.tsx | 29 ++ .../app/(auth)/dashboard/planning/page.tsx | 40 ++- .../dashboard/settings/media/_media.tsx | 169 ++++++++++++ .../(auth)/dashboard/settings/media/page.tsx | 180 ++----------- .../dashboard/settings/roles/_roles.tsx | 247 ++++++++++++++++++ .../(auth)/dashboard/settings/roles/page.tsx | 238 ++--------------- .../settings/shortcodes/_shortcodes.tsx | 70 +++++ .../dashboard/settings/shortcodes/page.tsx | 82 ++---- frontend/components/members-table.tsx | 60 +++-- frontend/components/planning.tsx | 2 +- frontend/components/shortcodes-table.tsx | 72 +++-- frontend/hooks/use-roles.tsx | 17 ++ frontend/interfaces/IUser.ts | 4 +- frontend/lib/getMe.ts | 22 ++ frontend/lib/hasPermissions.ts | 25 ++ frontend/middleware.ts | 10 +- frontend/types/types.tsx | 2 +- 26 files changed, 1071 insertions(+), 794 deletions(-) create mode 100644 frontend/app/(auth)/dashboard/members/[uuid]/_user.tsx create mode 100644 frontend/app/(auth)/dashboard/planning/_planning.tsx create mode 100644 frontend/app/(auth)/dashboard/settings/media/_media.tsx create mode 100644 frontend/app/(auth)/dashboard/settings/roles/_roles.tsx create mode 100644 frontend/app/(auth)/dashboard/settings/shortcodes/_shortcodes.tsx create mode 100644 frontend/hooks/use-roles.tsx create mode 100644 frontend/lib/getMe.ts create mode 100644 frontend/lib/hasPermissions.ts diff --git a/backend/api/blogs_routes.go b/backend/api/blogs_routes.go index a2fe32d..9f94bc2 100644 --- a/backend/api/blogs_routes.go +++ b/backend/api/blogs_routes.go @@ -7,8 +7,9 @@ import ( var BlogsRoutes = map[string]core.Handler{ "/blogs/new": { - Handler: blogs.HandleNew, - Middlewares: []core.Middleware{Methods(("POST")), AuthJWT}}, + Handler: blogs.HandleNew, + Middlewares: []core.Middleware{Methods(("POST")), + HasPermissions("blogs", "insert"), AuthJWT}}, "/blogs/{uuid}": { Handler: blogs.HandleBlog, Middlewares: []core.Middleware{Methods("GET")}}, diff --git a/backend/api/media_routes.go b/backend/api/media_routes.go index db5179b..9c50150 100644 --- a/backend/api/media_routes.go +++ b/backend/api/media_routes.go @@ -7,11 +7,13 @@ import ( var MediaRoutes = map[string]core.Handler{ "/media/upload": { - Handler: media.HandleUpload, - Middlewares: []core.Middleware{Methods("POST"), AuthJWT}}, + Handler: media.HandleUpload, + Middlewares: []core.Middleware{Methods("POST"), + HasPermissions("media", "insert"), AuthJWT}}, "/media/verify": { - Handler: media.HandleVerify, - Middlewares: []core.Middleware{Methods("POST"), AuthJWT}, + Handler: media.HandleVerify, + Middlewares: []core.Middleware{Methods("POST"), + HasPermissions("media", "insert"), AuthJWT}, }, // Paginated media response "/media": { @@ -29,11 +31,13 @@ var MediaRoutes = map[string]core.Handler{ Middlewares: []core.Middleware{Methods("GET")}, }, "/media/{media_uuid}/update": { - Handler: media.HandleUpdate, - Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT}, + Handler: media.HandleUpdate, + Middlewares: []core.Middleware{Methods("PATCH"), + HasPermissions("media", "update"), AuthJWT}, }, "/media/{media_uuid}/delete": { - Handler: media.HandleDelete, - Middlewares: []core.Middleware{Methods("DELETE"), AuthJWT}, + Handler: media.HandleDelete, + Middlewares: []core.Middleware{Methods("DELETE"), + HasPermissions("media", "delete"), AuthJWT}, }, } diff --git a/backend/api/permissions_routes.go b/backend/api/permissions_routes.go index 6f84b10..aee5c5e 100644 --- a/backend/api/permissions_routes.go +++ b/backend/api/permissions_routes.go @@ -7,19 +7,23 @@ import ( var PermissionsRoutes = map[string]core.Handler{ "/permissions": { - Handler: permissions.HandlePermissions, - Middlewares: []core.Middleware{Methods("GET"), AuthJWT}, + Handler: permissions.HandlePermissions, + Middlewares: []core.Middleware{Methods("GET"), + HasPermissions("permissions", "get"), AuthJWT}, }, "/permissions/grouped": { - Handler: permissions.HandleResourceActions, - Middlewares: []core.Middleware{Methods("GET"), AuthJWT}, + Handler: permissions.HandleResourceActions, + Middlewares: []core.Middleware{Methods("GET"), + HasPermissions("permissions", "get"), AuthJWT}, }, "/permissions/resources/{resource}": { - Handler: permissions.HandlePermissionsResource, - Middlewares: []core.Middleware{Methods("GET"), AuthJWT}, + Handler: permissions.HandlePermissionsResource, + Middlewares: []core.Middleware{Methods("GET"), + HasPermissions("permissions", "get"), AuthJWT}, }, "/permissions/resources/{resource}/{action}": { - Handler: permissions.HandlePermission, - Middlewares: []core.Middleware{Methods("GET"), AuthJWT}, + Handler: permissions.HandlePermission, + Middlewares: []core.Middleware{Methods("GET"), + HasPermissions("permissions", "get"), AuthJWT}, }, } diff --git a/backend/api/roles_routes.go b/backend/api/roles_routes.go index bcc9ae9..11690ba 100644 --- a/backend/api/roles_routes.go +++ b/backend/api/roles_routes.go @@ -7,35 +7,43 @@ import ( var RolesRoutes = map[string]core.Handler{ "/roles": { - Handler: roles.HandleRoles, - Middlewares: []core.Middleware{Methods("GET"), AuthJWT}, + Handler: roles.HandleRoles, + Middlewares: []core.Middleware{Methods("GET"), + HasPermissions("roles", "get"), AuthJWT}, }, "/roles/new": { - Handler: roles.HandleNew, - Middlewares: []core.Middleware{Methods("POST"), AuthJWT}, + Handler: roles.HandleNew, + Middlewares: []core.Middleware{Methods("POST"), + HasPermissions("roles", "insert"), AuthJWT}, }, "/roles/{role_uuid}": { - Handler: roles.HandleRole, - Middlewares: []core.Middleware{Methods("GET"), AuthJWT}, + Handler: roles.HandleRole, + Middlewares: []core.Middleware{Methods("GET"), + HasPermissions("roles", "get"), AuthJWT}, }, "/roles/{role_uuid}/update": { - Handler: roles.HandleUpdate, - Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT}, + Handler: roles.HandleUpdate, + Middlewares: []core.Middleware{Methods("PATCH"), + HasPermissions("roles", "update"), AuthJWT}, }, "/roles/{role_uuid}/delete": { - Handler: roles.HandleDelete, - Middlewares: []core.Middleware{Methods("DELETE"), AuthJWT}, + Handler: roles.HandleDelete, + Middlewares: []core.Middleware{Methods("DELETE"), + HasPermissions("roles", "delete"), AuthJWT}, }, "/roles/{role_uuid}/permissions/": { - Handler: roles.HandleRolePermissions, - Middlewares: []core.Middleware{Methods("GET"), AuthJWT}, + Handler: roles.HandleRolePermissions, + Middlewares: []core.Middleware{Methods("GET"), + HasPermissions("roles", "get"), AuthJWT}, }, "/roles/{role_uuid}/permissions/{resource}/{action}/add": { - Handler: roles.HandleAddPermission, - Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT}, + Handler: roles.HandleAddPermission, + Middlewares: []core.Middleware{Methods("PATCH"), + HasPermissions("roles", "update"), AuthJWT}, }, "/roles/{role_uuid}/permissions/{resource}/{action}/remove": { - Handler: roles.HandleRemovePermission, - Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT}, + Handler: roles.HandleRemovePermission, + Middlewares: []core.Middleware{Methods("PATCH"), + HasPermissions("roles", "update"), AuthJWT}, }, } diff --git a/backend/api/shortcodes_routes.go b/backend/api/shortcodes_routes.go index 08cb2ee..dd960e4 100644 --- a/backend/api/shortcodes_routes.go +++ b/backend/api/shortcodes_routes.go @@ -7,23 +7,27 @@ import ( var ShortcodesRoutes = map[string]core.Handler{ "/shortcodes/new": { - Handler: shortcodes.HandleNew, - Middlewares: []core.Middleware{Methods("POST"), AuthJWT}, + Handler: shortcodes.HandleNew, + Middlewares: []core.Middleware{Methods("POST"), + HasPermissions("shortcodes", "insert"), AuthJWT}, }, "/shortcodes": { - Handler: shortcodes.HandleShortcodes, - Middlewares: []core.Middleware{Methods("GET"), AuthJWT}, + Handler: shortcodes.HandleShortcodes, + Middlewares: []core.Middleware{Methods("GET"), + HasPermissions("shortcodes", "get"), AuthJWT}, }, "/shortcodes/{shortcode}": { Handler: shortcodes.HandleShortcode, Middlewares: []core.Middleware{Methods("GET")}, }, "/shortcodes/{shortcode}/delete": { - Handler: shortcodes.HandleDelete, - Middlewares: []core.Middleware{Methods("DELETE"), AuthJWT}, + Handler: shortcodes.HandleDelete, + Middlewares: []core.Middleware{Methods("DELETE"), + HasPermissions("shortcodes", "delete"), AuthJWT}, }, "/shortcodes/{shortcode}/update": { - Handler: shortcodes.HandleUpdate, - Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT}, + Handler: shortcodes.HandleUpdate, + Middlewares: []core.Middleware{Methods("PATCH"), + HasPermissions("shortcodes", "update"), AuthJWT}, }, } diff --git a/backend/api/users/user.go b/backend/api/users/user.go index 05c77d5..1b77b07 100644 --- a/backend/api/users/user.go +++ b/backend/api/users/user.go @@ -14,7 +14,7 @@ func HandleUser(w http.ResponseWriter, r *http.Request) { count, err := core.DB.NewSelect(). Model(&user). Where("user_id = ?", uuid). - Relation("Roles"). + Relation("Roles.Permissions"). Limit(1). ScanAndCount(context.Background()) diff --git a/frontend/app/(auth)/dashboard/members/[uuid]/_user.tsx b/frontend/app/(auth)/dashboard/members/[uuid]/_user.tsx new file mode 100644 index 0000000..9b7241a --- /dev/null +++ b/frontend/app/(auth)/dashboard/members/[uuid]/_user.tsx @@ -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(`/users/${uuid}`, {}, true); + + const availableRoles = useApi("/roles", {}, true); + availableRoles.data ??= []; + const [selectedRole, setSelectedRole] = useState(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

Error

; + + return ( +
+ + +
+
+ +
+

+ {_user.data.firstname} {_user.data.lastname} +

+

+ {_user.data.email} +

+
+
+ +
+
+

+ Rôles +

+
+ {_user.data.roles?.map((role) => ( + + {role.name} + {hasPermissions(user.roles, { + users: ["update"], + }) && ( + + )} + + ))} +
+ + {hasPermissions(user.roles, { + users: ["update"], + }) && ( +
+ + +
+ )} +
+ + {/*
+

+ Organizations +

+
+ {user.data.organizations.map((org) => ( + + {org} + + + ))} +
+
+ + +
+
*/} +
+
+
+
+
+ ); +} diff --git a/frontend/app/(auth)/dashboard/members/[uuid]/page.tsx b/frontend/app/(auth)/dashboard/members/[uuid]/page.tsx index 5c6b08c..8b92c2d 100644 --- a/frontend/app/(auth)/dashboard/members/[uuid]/page.tsx +++ b/frontend/app/(auth)/dashboard/members/[uuid]/page.tsx @@ -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(`/users/${uuid}`, {}, true); - - const availableRoles = useApi("/roles", {}, true); - availableRoles.data ??= []; - const [selectedRole, setSelectedRole] = useState(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

Error

; - - return ( -
- - -
-
- -
-

- {user.data.firstname} {user.data.lastname} -

-

- {user.data.email} -

-
-
- -
-
-

- Rôles -

-
- {user.data.roles?.map((role) => ( - - {role.name} - - - ))} -
- -
- - -
-
- - {/*
-

- Organizations -

-
- {user.data.organizations.map((org) => ( - - {org} - - - ))} -
-
- - -
-
*/} -
-
-
-
-
- ); + return ; } diff --git a/frontend/app/(auth)/dashboard/members/page.tsx b/frontend/app/(auth)/dashboard/members/page.tsx index 771ed9d..c75379c 100644 --- a/frontend/app/(auth)/dashboard/members/page.tsx +++ b/frontend/app/(auth)/dashboard/members/page.tsx @@ -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 (
- +
); } diff --git a/frontend/app/(auth)/dashboard/planning/_planning.tsx b/frontend/app/(auth)/dashboard/planning/_planning.tsx new file mode 100644 index 0000000..baa44d4 --- /dev/null +++ b/frontend/app/(auth)/dashboard/planning/_planning.tsx @@ -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("/events", undefined, false, false); + + if (isLoading) return ; + if (success) + return ( + + ); +} diff --git a/frontend/app/(auth)/dashboard/planning/page.tsx b/frontend/app/(auth)/dashboard/planning/page.tsx index 4079b32..f5d326e 100644 --- a/frontend/app/(auth)/dashboard/planning/page.tsx +++ b/frontend/app/(auth)/dashboard/planning/page.tsx @@ -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("/events", undefined, false, false); - - if (isLoading) return ; - if (success) - return ( - - ); + return ; } diff --git a/frontend/app/(auth)/dashboard/settings/media/_media.tsx b/frontend/app/(auth)/dashboard/settings/media/_media.tsx new file mode 100644 index 0000000..34f5d2a --- /dev/null +++ b/frontend/app/(auth)/dashboard/settings/media/_media.tsx @@ -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(null); + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const { progress, isUploading, error, uploadFile, cancelUpload } = + useFileUpload(); + + const handleAddPhoto = (newPhoto: Omit, file: File) => { + uploadFile(file, "/media/upload", (response) => { + mutate(); + }); + }; + + const handleUpdatePhoto = async ( + body: Media | Omit, + file: File, + ) => { + if (selectedPhoto) { + const res = await request( + `/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(`/media/${id}/delete`, { + method: "DELETE", + requiresAuth: true, + }); + if (res.status === "Success") mutate(); + } catch (e) { + console.log(e); + } + setSelectedPhoto(null); + }; + + return ( +
+
+

Gallerie Photo

+ +
+
+ {data?.items.map((photo) => ( +
setSelectedPhoto(photo)} + > + {photo.alt} +
+ ))} +
+ + + + { + e.preventDefault(); + setPage((prev) => Math.max(prev - 1, 1)); + }} + className={ + data?.page === 1 + ? "pointer-events-none opacity-50" + : "" + } + /> + + {[...Array(data?.totalPages)].map((_, i) => ( + + { + e.preventDefault(); + setPage(i + 1); + }} + isActive={data?.page === i + 1} + > + {i + 1} + + + ))} + + { + e.preventDefault(); + setPage((prev) => + Math.min(prev + 1, data?.totalPages ?? 1), + ); + }} + className={ + data?.page === data?.totalPages + ? "pointer-events-none opacity-50" + : "" + } + /> + + + + + setSelectedPhoto((p) => (isUploading ? p : null)) + } + onDelete={handleDeletePhoto} + onSave={handleUpdatePhoto} + /> + setIsAddDialogOpen(false)} + onSave={handleAddPhoto} + /> +
+ ); +} diff --git a/frontend/app/(auth)/dashboard/settings/media/page.tsx b/frontend/app/(auth)/dashboard/settings/media/page.tsx index 0775abd..810e001 100644 --- a/frontend/app/(auth)/dashboard/settings/media/page.tsx +++ b/frontend/app/(auth)/dashboard/settings/media/page.tsx @@ -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(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, file: File) => { - uploadFile(file, "/media/upload", (response) => { - mutate(); - }); - }; - - const handleUpdatePhoto = async ( - body: Media | Omit, - file: File, - ) => { - if (selectedPhoto) { - const res = await request( - `/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(`/media/${id}/delete`, { - method: "DELETE", - requiresAuth: true, - }); - if (res.status === "Success") mutate(); - } catch (e) { - console.log(e); - } - setSelectedPhoto(null); - }; - - return ( -
-
-

Gallerie Photo

- -
-
- {data?.items.map((photo) => ( -
setSelectedPhoto(photo)} - > - {photo.alt} -
- ))} -
- - - - { - e.preventDefault(); - setPage((prev) => Math.max(prev - 1, 1)); - }} - className={ - data?.page === 1 - ? "pointer-events-none opacity-50" - : "" - } - /> - - {[...Array(data?.totalPages)].map((_, i) => ( - - { - e.preventDefault(); - setPage(i + 1); - }} - isActive={data?.page === i + 1} - > - {i + 1} - - - ))} - - { - e.preventDefault(); - setPage((prev) => - Math.min(prev + 1, data?.totalPages ?? 1), - ); - }} - className={ - data?.page === data?.totalPages - ? "pointer-events-none opacity-50" - : "" - } - /> - - - - - setSelectedPhoto((p) => (isUploading ? p : null)) - } - onDelete={handleDeletePhoto} - onSave={handleUpdatePhoto} - /> - setIsAddDialogOpen(false)} - onSave={handleAddPhoto} - /> -
- ); + return ; } diff --git a/frontend/app/(auth)/dashboard/settings/roles/_roles.tsx b/frontend/app/(auth)/dashboard/settings/roles/_roles.tsx new file mode 100644 index 0000000..219232c --- /dev/null +++ b/frontend/app/(auth)/dashboard/settings/roles/_roles.tsx @@ -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(""); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + const { data: permissions } = useApi( + "/permissions/grouped", + {}, + true, + ); + + const { data: roles, mutate: rolesMutate } = useApi( + "/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 ( +
+
+

Rôles et Permissions

+ {hasPermissions(user.roles, { roles: ["insert"] }) && ( + + + + + + + Nouveau rôle + +
+ + setNewRoleName(e.target.value) + } + /> +
+ + + +
+
+ )} +
+ {permissions && + roles && + roles.map((role, index) => ( + deleteRole(role.id)} + /> + ))} +
+ ); +} + +interface RoleCardProps { + user: IUser; + role: Role; + onDelete: () => void; + permissions: PermissionsGrouped; +} + +function RoleCard({ role, onDelete, permissions, user }: RoleCardProps) { + return ( + + + {toTitleCase(role.name)} + + + + {Object.entries(permissions).map(([res, actions]) => { + return ( + + ); + })} + + + ); +} + +interface ResourceSectionProps { + disabled?: boolean; + resource: string; + defaultActions: string[]; + role: Role; +} + +function ResourceSection({ + resource, + defaultActions, + role, + disabled = false, +}: ResourceSectionProps) { + const [isExpanded, setIsExpanded] = useState(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 ( +
+ { + 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}`} + /> + +
+ ); + }; + + return ( +
+ + {isExpanded && ( +
+ {defaultActions.map((action) => ( + + ))} +
+ )} +
+ ); +} diff --git a/frontend/app/(auth)/dashboard/settings/roles/page.tsx b/frontend/app/(auth)/dashboard/settings/roles/page.tsx index e31e8b4..d2e1ef4 100644 --- a/frontend/app/(auth)/dashboard/settings/roles/page.tsx +++ b/frontend/app/(auth)/dashboard/settings/roles/page.tsx @@ -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(""); - const [isDialogOpen, setIsDialogOpen] = useState(false); - - const { data: permissions } = useApi( - "/permissions/grouped", - {}, - true, - ); - - const { data: roles, mutate: rolesMutate } = useApi( - "/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 ( -
-
-

Rôles et Permissions

- - - - - - - Nouveau rôle - -
- setNewRoleName(e.target.value)} - /> -
- - - -
-
-
- {permissions && - roles && - roles.map((role, index) => ( - deleteRole(role.id)} - /> - ))} -
- ); -} - -interface RoleCardProps { - role: Role; - onDelete: () => void; - permissions: PermissionsGrouped; -} - -function RoleCard({ role, onDelete, permissions }: RoleCardProps) { - return ( - - - {toTitleCase(role.name)} - - - - {Object.entries(permissions).map(([res, actions]) => { - return ( - - ); - })} - - - ); -} - -interface ResourceSectionProps { - resource: string; - defaultActions: string[]; - role: Role; -} - -function ResourceSection({ - resource, - defaultActions, - role, -}: ResourceSectionProps) { - const [isExpanded, setIsExpanded] = useState(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 ( -
- { - 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}`} - /> - -
- ); - }; - - return ( -
- - {isExpanded && ( -
- {defaultActions.map((action) => ( - - ))} -
- )} -
- ); + return ; } diff --git a/frontend/app/(auth)/dashboard/settings/shortcodes/_shortcodes.tsx b/frontend/app/(auth)/dashboard/settings/shortcodes/_shortcodes.tsx new file mode 100644 index 0000000..ff2d3c1 --- /dev/null +++ b/frontend/app/(auth)/dashboard/settings/shortcodes/_shortcodes.tsx @@ -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("/shortcodes", undefined, true); + + console.log(shortcodes); + + const handleUpdate = async (updatedShortcode: IShortcode) => { + const res = await request( + `/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(`/shortcodes/${code}/delete`, { + requiresAuth: true, + method: "DELETE", + }); + mutate(); + }; + + const handleAdd = async (newShortcode: Omit) => { + const res = await request(`/shortcodes/new`, { + body: newShortcode, + method: "POST", + requiresAuth: true, + }); + console.log(res); + mutate(); + }; + + return ( +
+

Shortcodes

+ {isLoading && ( + + )} + {error &&

{error}

} + +
+ ); +} diff --git a/frontend/app/(auth)/dashboard/settings/shortcodes/page.tsx b/frontend/app/(auth)/dashboard/settings/shortcodes/page.tsx index c585d19..de9a1f5 100644 --- a/frontend/app/(auth)/dashboard/settings/shortcodes/page.tsx +++ b/frontend/app/(auth)/dashboard/settings/shortcodes/page.tsx @@ -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("/shortcodes", undefined, true); - - console.log(shortcodes); - - const handleUpdate = async (updatedShortcode: IShortcode) => { - const res = await request( - `/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(`/shortcodes/${code}/delete`, { - requiresAuth: true, - method: "DELETE", - }); - mutate(); - }; - - const handleAdd = async (newShortcode: Omit) => { - const res = await request(`/shortcodes/new`, { - body: newShortcode, - method: "POST", - requiresAuth: true, - }); - console.log(res); - mutate(); - }; - - return ( -
-

Shortcodes

- {isLoading && ( - - )} - {error &&

{error}

} - -
- ); + return ; } diff --git a/frontend/components/members-table.tsx b/frontend/components/members-table.tsx index 898d619..75bcb51 100644 --- a/frontend/components/members-table.tsx +++ b/frontend/components/members-table.tsx @@ -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() { - + {hasPermissions(user.roles, { users: ["insert"] }) && ( + + )}
@@ -162,25 +166,35 @@ export default function MembersTable() { {member.phone} {member.role} - - + {hasPermissions(user.roles, { + users: ["update"], + }) && ( + + )} + {hasPermissions(user.roles, { + users: ["delete"], + }) && ( + + )} ))} diff --git a/frontend/components/planning.tsx b/frontend/components/planning.tsx index a24bc75..4500392 100644 --- a/frontend/components/planning.tsx +++ b/frontend/components/planning.tsx @@ -75,7 +75,7 @@ const Planning: React.FC<{ description: res.message, }); } else { - mutate?.(); + // mutate?.(); } } catch (e) { if (e instanceof Error) diff --git a/frontend/components/shortcodes-table.tsx b/frontend/components/shortcodes-table.tsx index 95b30e5..aa69b32 100644 --- a/frontend/components/shortcodes-table.tsx +++ b/frontend/components/shortcodes-table.tsx @@ -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( null, @@ -39,20 +43,22 @@ export function ShortcodeTable({ const [addDialog, setAddDialog] = useState(false); return (
-
- - setAddDialog(false)} - /> -
+ {hasPermissions(user.roles, { shortcodes: ["insert"] }) && ( +
+ + setAddDialog(false)} + /> +
+ )}
@@ -91,20 +97,30 @@ export function ShortcodeTable({ - - setUpdateDialog(shortcode) - } - > - Mettre à jour - - - onDelete(shortcode.code) - } - > - Supprimer - + {hasPermissions(user.roles, { + shortcodes: ["update"], + }) && ( + + setUpdateDialog( + shortcode, + ) + } + > + Mettre à jour + + )} + {hasPermissions(user.roles, { + shortcodes: ["delete"], + }) && ( + + onDelete(shortcode.code) + } + > + Supprimer + + )} diff --git a/frontend/hooks/use-roles.tsx b/frontend/hooks/use-roles.tsx new file mode 100644 index 0000000..ffbc0c8 --- /dev/null +++ b/frontend/hooks/use-roles.tsx @@ -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(null); + const cookie = getCookie("auth_token"); + useEffect(() => { + if (cookie) + getMe(cookie?.toString()).then((me) => { + setRoles(me?.data?.roles ?? null); + }); + }, []); + + return roles; +} diff --git a/frontend/interfaces/IUser.ts b/frontend/interfaces/IUser.ts index 7b593fe..5d3defb 100644 --- a/frontend/interfaces/IUser.ts +++ b/frontend/interfaces/IUser.ts @@ -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; } diff --git a/frontend/lib/getMe.ts b/frontend/lib/getMe.ts new file mode 100644 index 0000000..fa7e04e --- /dev/null +++ b/frontend/lib/getMe.ts @@ -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 | 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; diff --git a/frontend/lib/hasPermissions.ts b/frontend/lib/hasPermissions.ts new file mode 100644 index 0000000..ebfc135 --- /dev/null +++ b/frontend/lib/hasPermissions.ts @@ -0,0 +1,25 @@ +import { Role } from "@/types/types"; + +export default function hasPermissions( + roles: Role[], + permissions: { [key: string]: string[] }, +) { + const permissionsSet: Map = 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; +} diff --git a/frontend/middleware.ts b/frontend/middleware.ts index 436f0a0..1bd9a25 100644 --- a/frontend/middleware.ts +++ b/frontend/middleware.ts @@ -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 = 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( diff --git a/frontend/types/types.tsx b/frontend/types/types.tsx index 6a630db..a33ba6c 100644 --- a/frontend/types/types.tsx +++ b/frontend/types/types.tsx @@ -6,7 +6,7 @@ export interface Permission { export interface Role { id: string; name: string; - permissions: Permission[]; + permissions?: Permission[]; } // Status type as a string literal From ebe3b88035c23c414c62771bbaecdc0140048ee5 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:35:59 +0100 Subject: [PATCH 2/2] Redirect to implemented --- frontend/components/login-form.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/components/login-form.tsx b/frontend/components/login-form.tsx index 427d62a..0ee4348 100644 --- a/frontend/components/login-form.tsx +++ b/frontend/components/login-form.tsx @@ -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) => { 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);