Merge branch 'dev/cedric' into dev/guerby
This commit is contained in:
@@ -7,8 +7,9 @@ import (
|
|||||||
|
|
||||||
var BlogsRoutes = map[string]core.Handler{
|
var BlogsRoutes = map[string]core.Handler{
|
||||||
"/blogs/new": {
|
"/blogs/new": {
|
||||||
Handler: blogs.HandleNew,
|
Handler: blogs.HandleNew,
|
||||||
Middlewares: []core.Middleware{Methods(("POST")), AuthJWT}},
|
Middlewares: []core.Middleware{Methods(("POST")),
|
||||||
|
HasPermissions("blogs", "insert"), AuthJWT}},
|
||||||
"/blogs/{uuid}": {
|
"/blogs/{uuid}": {
|
||||||
Handler: blogs.HandleBlog,
|
Handler: blogs.HandleBlog,
|
||||||
Middlewares: []core.Middleware{Methods("GET")}},
|
Middlewares: []core.Middleware{Methods("GET")}},
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import (
|
|||||||
|
|
||||||
var MediaRoutes = map[string]core.Handler{
|
var MediaRoutes = map[string]core.Handler{
|
||||||
"/media/upload": {
|
"/media/upload": {
|
||||||
Handler: media.HandleUpload,
|
Handler: media.HandleUpload,
|
||||||
Middlewares: []core.Middleware{Methods("POST"), AuthJWT}},
|
Middlewares: []core.Middleware{Methods("POST"),
|
||||||
|
HasPermissions("media", "insert"), AuthJWT}},
|
||||||
"/media/verify": {
|
"/media/verify": {
|
||||||
Handler: media.HandleVerify,
|
Handler: media.HandleVerify,
|
||||||
Middlewares: []core.Middleware{Methods("POST"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("POST"),
|
||||||
|
HasPermissions("media", "insert"), AuthJWT},
|
||||||
},
|
},
|
||||||
// Paginated media response
|
// Paginated media response
|
||||||
"/media": {
|
"/media": {
|
||||||
@@ -29,11 +31,13 @@ var MediaRoutes = map[string]core.Handler{
|
|||||||
Middlewares: []core.Middleware{Methods("GET")},
|
Middlewares: []core.Middleware{Methods("GET")},
|
||||||
},
|
},
|
||||||
"/media/{media_uuid}/update": {
|
"/media/{media_uuid}/update": {
|
||||||
Handler: media.HandleUpdate,
|
Handler: media.HandleUpdate,
|
||||||
Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("PATCH"),
|
||||||
|
HasPermissions("media", "update"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/media/{media_uuid}/delete": {
|
"/media/{media_uuid}/delete": {
|
||||||
Handler: media.HandleDelete,
|
Handler: media.HandleDelete,
|
||||||
Middlewares: []core.Middleware{Methods("DELETE"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("DELETE"),
|
||||||
|
HasPermissions("media", "delete"), AuthJWT},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,19 +7,23 @@ import (
|
|||||||
|
|
||||||
var PermissionsRoutes = map[string]core.Handler{
|
var PermissionsRoutes = map[string]core.Handler{
|
||||||
"/permissions": {
|
"/permissions": {
|
||||||
Handler: permissions.HandlePermissions,
|
Handler: permissions.HandlePermissions,
|
||||||
Middlewares: []core.Middleware{Methods("GET"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("GET"),
|
||||||
|
HasPermissions("permissions", "get"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/permissions/grouped": {
|
"/permissions/grouped": {
|
||||||
Handler: permissions.HandleResourceActions,
|
Handler: permissions.HandleResourceActions,
|
||||||
Middlewares: []core.Middleware{Methods("GET"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("GET"),
|
||||||
|
HasPermissions("permissions", "get"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/permissions/resources/{resource}": {
|
"/permissions/resources/{resource}": {
|
||||||
Handler: permissions.HandlePermissionsResource,
|
Handler: permissions.HandlePermissionsResource,
|
||||||
Middlewares: []core.Middleware{Methods("GET"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("GET"),
|
||||||
|
HasPermissions("permissions", "get"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/permissions/resources/{resource}/{action}": {
|
"/permissions/resources/{resource}/{action}": {
|
||||||
Handler: permissions.HandlePermission,
|
Handler: permissions.HandlePermission,
|
||||||
Middlewares: []core.Middleware{Methods("GET"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("GET"),
|
||||||
|
HasPermissions("permissions", "get"), AuthJWT},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,35 +7,43 @@ import (
|
|||||||
|
|
||||||
var RolesRoutes = map[string]core.Handler{
|
var RolesRoutes = map[string]core.Handler{
|
||||||
"/roles": {
|
"/roles": {
|
||||||
Handler: roles.HandleRoles,
|
Handler: roles.HandleRoles,
|
||||||
Middlewares: []core.Middleware{Methods("GET"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("GET"),
|
||||||
|
HasPermissions("roles", "get"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/roles/new": {
|
"/roles/new": {
|
||||||
Handler: roles.HandleNew,
|
Handler: roles.HandleNew,
|
||||||
Middlewares: []core.Middleware{Methods("POST"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("POST"),
|
||||||
|
HasPermissions("roles", "insert"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/roles/{role_uuid}": {
|
"/roles/{role_uuid}": {
|
||||||
Handler: roles.HandleRole,
|
Handler: roles.HandleRole,
|
||||||
Middlewares: []core.Middleware{Methods("GET"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("GET"),
|
||||||
|
HasPermissions("roles", "get"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/roles/{role_uuid}/update": {
|
"/roles/{role_uuid}/update": {
|
||||||
Handler: roles.HandleUpdate,
|
Handler: roles.HandleUpdate,
|
||||||
Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("PATCH"),
|
||||||
|
HasPermissions("roles", "update"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/roles/{role_uuid}/delete": {
|
"/roles/{role_uuid}/delete": {
|
||||||
Handler: roles.HandleDelete,
|
Handler: roles.HandleDelete,
|
||||||
Middlewares: []core.Middleware{Methods("DELETE"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("DELETE"),
|
||||||
|
HasPermissions("roles", "delete"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/roles/{role_uuid}/permissions/": {
|
"/roles/{role_uuid}/permissions/": {
|
||||||
Handler: roles.HandleRolePermissions,
|
Handler: roles.HandleRolePermissions,
|
||||||
Middlewares: []core.Middleware{Methods("GET"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("GET"),
|
||||||
|
HasPermissions("roles", "get"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/roles/{role_uuid}/permissions/{resource}/{action}/add": {
|
"/roles/{role_uuid}/permissions/{resource}/{action}/add": {
|
||||||
Handler: roles.HandleAddPermission,
|
Handler: roles.HandleAddPermission,
|
||||||
Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("PATCH"),
|
||||||
|
HasPermissions("roles", "update"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/roles/{role_uuid}/permissions/{resource}/{action}/remove": {
|
"/roles/{role_uuid}/permissions/{resource}/{action}/remove": {
|
||||||
Handler: roles.HandleRemovePermission,
|
Handler: roles.HandleRemovePermission,
|
||||||
Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("PATCH"),
|
||||||
|
HasPermissions("roles", "update"), AuthJWT},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,23 +7,27 @@ import (
|
|||||||
|
|
||||||
var ShortcodesRoutes = map[string]core.Handler{
|
var ShortcodesRoutes = map[string]core.Handler{
|
||||||
"/shortcodes/new": {
|
"/shortcodes/new": {
|
||||||
Handler: shortcodes.HandleNew,
|
Handler: shortcodes.HandleNew,
|
||||||
Middlewares: []core.Middleware{Methods("POST"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("POST"),
|
||||||
|
HasPermissions("shortcodes", "insert"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/shortcodes": {
|
"/shortcodes": {
|
||||||
Handler: shortcodes.HandleShortcodes,
|
Handler: shortcodes.HandleShortcodes,
|
||||||
Middlewares: []core.Middleware{Methods("GET"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("GET"),
|
||||||
|
HasPermissions("shortcodes", "get"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/shortcodes/{shortcode}": {
|
"/shortcodes/{shortcode}": {
|
||||||
Handler: shortcodes.HandleShortcode,
|
Handler: shortcodes.HandleShortcode,
|
||||||
Middlewares: []core.Middleware{Methods("GET")},
|
Middlewares: []core.Middleware{Methods("GET")},
|
||||||
},
|
},
|
||||||
"/shortcodes/{shortcode}/delete": {
|
"/shortcodes/{shortcode}/delete": {
|
||||||
Handler: shortcodes.HandleDelete,
|
Handler: shortcodes.HandleDelete,
|
||||||
Middlewares: []core.Middleware{Methods("DELETE"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("DELETE"),
|
||||||
|
HasPermissions("shortcodes", "delete"), AuthJWT},
|
||||||
},
|
},
|
||||||
"/shortcodes/{shortcode}/update": {
|
"/shortcodes/{shortcode}/update": {
|
||||||
Handler: shortcodes.HandleUpdate,
|
Handler: shortcodes.HandleUpdate,
|
||||||
Middlewares: []core.Middleware{Methods("PATCH"), AuthJWT},
|
Middlewares: []core.Middleware{Methods("PATCH"),
|
||||||
|
HasPermissions("shortcodes", "update"), AuthJWT},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func HandleUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
count, err := core.DB.NewSelect().
|
count, err := core.DB.NewSelect().
|
||||||
Model(&user).
|
Model(&user).
|
||||||
Where("user_id = ?", uuid).
|
Where("user_id = ?", uuid).
|
||||||
Relation("Roles").
|
Relation("Roles.Permissions").
|
||||||
Limit(1).
|
Limit(1).
|
||||||
ScanAndCount(context.Background())
|
ScanAndCount(context.Background())
|
||||||
|
|
||||||
|
|||||||
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";
|
return <UserDetailsPage user={me.data} />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import MembersTable from "@/components/members-table";
|
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({}) {
|
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 (
|
return (
|
||||||
<div className="container mx-auto px-4 py-10">
|
<div className="container mx-auto px-4 py-10">
|
||||||
<MembersTable />
|
<MembersTable user={me.data} />
|
||||||
</div>
|
</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";
|
export default async function Page() {
|
||||||
import { useApi } from "@/hooks/use-api";
|
const me = await getMe();
|
||||||
import ICalendarEvent from "@/interfaces/ICalendarEvent";
|
if (
|
||||||
import { Loader2 } from "lucide-react";
|
!me ||
|
||||||
|
me.status === "Error" ||
|
||||||
|
!me.data ||
|
||||||
|
!hasPermissions(me.data.roles, {
|
||||||
|
events: ["get"],
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
redirect("/dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
export default function Page() {
|
return <PlanningPage user={me.data} />;
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
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 getMe from "@/lib/getMe";
|
||||||
import Image from "next/image";
|
import { redirect } from "next/navigation";
|
||||||
import { Plus } from "lucide-react";
|
import PhotoGallery from "./_media";
|
||||||
import { Button } from "@/components/ui/button";
|
import hasPermissions from "@/lib/hasPermissions";
|
||||||
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";
|
|
||||||
|
|
||||||
export default function PhotoGallery() {
|
export default async function Page() {
|
||||||
const {
|
const me = await getMe();
|
||||||
data,
|
if (
|
||||||
error: mediaError,
|
!me ||
|
||||||
isLoading,
|
me.status === "Error" ||
|
||||||
success,
|
!me.data ||
|
||||||
setPage,
|
!hasPermissions(me.data.roles, { media: ["get"] })
|
||||||
setLimit,
|
) {
|
||||||
mutate,
|
redirect("/dashboard");
|
||||||
} = 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) => {
|
return <PhotoGallery user={me.data} />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
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";
|
export default async function Page() {
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
const me = await getMe();
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
if (
|
||||||
import { Button } from "@/components/ui/button";
|
!me ||
|
||||||
import { Input } from "@/components/ui/input";
|
me.status === "Error" ||
|
||||||
import {
|
!me.data ||
|
||||||
Dialog,
|
!hasPermissions(me.data.roles, {
|
||||||
DialogContent,
|
roles: ["get"],
|
||||||
DialogHeader,
|
permissions: ["get"],
|
||||||
DialogTitle,
|
})
|
||||||
DialogTrigger,
|
) {
|
||||||
DialogFooter,
|
redirect("/dashboard");
|
||||||
} 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";
|
|
||||||
|
|
||||||
type Action = string;
|
return <RolesAndPermissions user={me.data} />;
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
export default async function Page() {
|
||||||
import { ShortcodeTable } from "@/components/shortcodes-table";
|
const me = await getMe();
|
||||||
import type IShortcode from "@/interfaces/IShortcode";
|
if (
|
||||||
import { useApi } from "@/hooks/use-api";
|
!me ||
|
||||||
import request from "@/lib/request";
|
me.status === "Error" ||
|
||||||
import { Loader2 } from "lucide-react";
|
!me.data ||
|
||||||
|
!hasPermissions(me.data.roles, {
|
||||||
|
shortcodes: ["get"],
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
redirect("/dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
export default function ShortcodesPage() {
|
return <ShortcodesPage user={me.data} />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import useLogin from "@/hooks/use-login";
|
import useLogin from "@/hooks/use-login";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
@@ -16,12 +16,20 @@ export function LoginForm({
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const { login, loading, isSuccess } = useLogin();
|
const { login, loading, isSuccess } = useLogin();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const res = await login({ email, password });
|
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);
|
console.log(res);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.log(err.message);
|
console.log(err.message);
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ import {
|
|||||||
UserRoundPlus,
|
UserRoundPlus,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
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 {
|
const {
|
||||||
data: members,
|
data: members,
|
||||||
error,
|
error,
|
||||||
@@ -97,9 +99,11 @@ export default function MembersTable() {
|
|||||||
<Button onClick={toggleSelectMode}>
|
<Button onClick={toggleSelectMode}>
|
||||||
{selectMode ? <CircleX /> : "Selectionner"}
|
{selectMode ? <CircleX /> : "Selectionner"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => handleOpenDialog(null)}>
|
{hasPermissions(user.roles, { users: ["insert"] }) && (
|
||||||
<UserRoundPlus />
|
<Button onClick={() => handleOpenDialog(null)}>
|
||||||
</Button>
|
<UserRoundPlus />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<ScrollArea className="h-full rounded-md border">
|
<ScrollArea className="h-full rounded-md border">
|
||||||
@@ -162,25 +166,35 @@ export default function MembersTable() {
|
|||||||
<TableCell>{member.phone}</TableCell>
|
<TableCell>{member.phone}</TableCell>
|
||||||
<TableCell>{member.role}</TableCell>
|
<TableCell>{member.role}</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Button
|
{hasPermissions(user.roles, {
|
||||||
variant="outline"
|
users: ["update"],
|
||||||
size="sm"
|
}) && (
|
||||||
className="mr-2"
|
<Button
|
||||||
onClick={() =>
|
variant="outline"
|
||||||
handleOpenDialog(member)
|
size="sm"
|
||||||
}
|
className="mr-2"
|
||||||
>
|
onClick={() =>
|
||||||
<UserRoundPen />
|
handleOpenDialog(member)
|
||||||
</Button>
|
}
|
||||||
<Button
|
>
|
||||||
variant="destructive"
|
<UserRoundPen />
|
||||||
size="sm"
|
</Button>
|
||||||
onClick={() =>
|
)}
|
||||||
handleDelete(member.userId!)
|
{hasPermissions(user.roles, {
|
||||||
}
|
users: ["delete"],
|
||||||
>
|
}) && (
|
||||||
<Trash2 />
|
<Button
|
||||||
</Button>
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleDelete(
|
||||||
|
member.userId!,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const Planning: React.FC<{
|
|||||||
description: res.message,
|
description: res.message,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
mutate?.();
|
// mutate?.();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error)
|
if (e instanceof Error)
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ import { MoreHorizontal } from "lucide-react";
|
|||||||
import type IShortcode from "@/interfaces/IShortcode";
|
import type IShortcode from "@/interfaces/IShortcode";
|
||||||
import ShortcodeDialog from "@/components/shortcode-dialogue";
|
import ShortcodeDialog from "@/components/shortcode-dialogue";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import IUser from "@/interfaces/IUser";
|
||||||
|
import hasPermissions from "@/lib/hasPermissions";
|
||||||
|
|
||||||
interface ShortcodeTableProps {
|
interface ShortcodeTableProps {
|
||||||
|
user: IUser;
|
||||||
shortcodes: IShortcode[];
|
shortcodes: IShortcode[];
|
||||||
onUpdate: (shortcode: IShortcode) => void;
|
onUpdate: (shortcode: IShortcode) => void;
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
@@ -32,6 +35,7 @@ export function ShortcodeTable({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
onDelete,
|
onDelete,
|
||||||
onAdd,
|
onAdd,
|
||||||
|
user,
|
||||||
}: ShortcodeTableProps) {
|
}: ShortcodeTableProps) {
|
||||||
const [shortcodeSelected, setUpdateDialog] = useState<IShortcode | null>(
|
const [shortcodeSelected, setUpdateDialog] = useState<IShortcode | null>(
|
||||||
null,
|
null,
|
||||||
@@ -39,20 +43,22 @@ export function ShortcodeTable({
|
|||||||
const [addDialog, setAddDialog] = useState<boolean>(false);
|
const [addDialog, setAddDialog] = useState<boolean>(false);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4">
|
{hasPermissions(user.roles, { shortcodes: ["insert"] }) && (
|
||||||
<Button
|
<div className="mb-4">
|
||||||
onClick={() => {
|
<Button
|
||||||
setAddDialog(true);
|
onClick={() => {
|
||||||
}}
|
setAddDialog(true);
|
||||||
>
|
}}
|
||||||
Ajouter
|
>
|
||||||
</Button>
|
Ajouter
|
||||||
<ShortcodeDialog
|
</Button>
|
||||||
onSave={onAdd}
|
<ShortcodeDialog
|
||||||
open={addDialog}
|
onSave={onAdd}
|
||||||
setOpen={() => setAddDialog(false)}
|
open={addDialog}
|
||||||
/>
|
setOpen={() => setAddDialog(false)}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -91,20 +97,30 @@ export function ShortcodeTable({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem
|
{hasPermissions(user.roles, {
|
||||||
onClick={() =>
|
shortcodes: ["update"],
|
||||||
setUpdateDialog(shortcode)
|
}) && (
|
||||||
}
|
<DropdownMenuItem
|
||||||
>
|
onClick={() =>
|
||||||
Mettre à jour
|
setUpdateDialog(
|
||||||
</DropdownMenuItem>
|
shortcode,
|
||||||
<DropdownMenuItem
|
)
|
||||||
onClick={() =>
|
}
|
||||||
onDelete(shortcode.code)
|
>
|
||||||
}
|
Mettre à jour
|
||||||
>
|
</DropdownMenuItem>
|
||||||
Supprimer
|
)}
|
||||||
</DropdownMenuItem>
|
{hasPermissions(user.roles, {
|
||||||
|
shortcodes: ["delete"],
|
||||||
|
}) && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
onDelete(shortcode.code)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</TableCell>
|
</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 {
|
export default interface IUser {
|
||||||
userId: string;
|
userId: string;
|
||||||
firstname: string;
|
firstname: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
role: string;
|
roles: Role[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: 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 { NextRequest, NextResponse } from "next/server";
|
||||||
import { ApiResponse } from "./types/types";
|
|
||||||
import { API_URL } from "./lib/constants";
|
import { API_URL } from "./lib/constants";
|
||||||
import IUser from "./interfaces/IUser";
|
import getMe from "./lib/getMe";
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
export async function middleware(request: NextRequest) {
|
||||||
const sessionCookie = request.cookies.get("auth_token")?.value;
|
const sessionCookie = request.cookies.get("auth_token")?.value;
|
||||||
@@ -17,11 +16,8 @@ export async function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(API_URL);
|
console.log(API_URL);
|
||||||
const res = await fetch(`${API_URL}/users/me`, {
|
const js = await getMe(sessionCookie);
|
||||||
headers: { Authorization: `Bearer ${sessionCookie}` },
|
if (js?.status === "Error") {
|
||||||
});
|
|
||||||
const js: ApiResponse<IUser> = await res.json();
|
|
||||||
if (js.status === "Error") {
|
|
||||||
console.log(js.message);
|
console.log(js.message);
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL(
|
new URL(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export interface Permission {
|
|||||||
export interface Role {
|
export interface Role {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
permissions: Permission[];
|
permissions?: Permission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status type as a string literal
|
// Status type as a string literal
|
||||||
|
|||||||
Reference in New Issue
Block a user