From be12220574716ef9097f171f6fe9147fc46ff853 Mon Sep 17 00:00:00 2001 From: charles-gauthereau Date: Wed, 10 Jun 2026 21:10:33 +0200 Subject: [PATCH] fix: demo acl --- app/(customer)/dashboard/layout.tsx | 22 +++--- src/features/profile/avatar-with-upload.tsx | 77 +++++++++++--------- src/features/profile/profile-general.tsx | 9 +-- src/features/profile/profile-security.tsx | 2 + src/features/profile/profile.action.ts | 2 +- src/lib/acl/acl-context.tsx | 33 +++++++++ src/lib/acl/system-acl.ts | 78 ++++++++++----------- 7 files changed, 138 insertions(+), 85 deletions(-) create mode 100644 src/lib/acl/acl-context.tsx diff --git a/app/(customer)/dashboard/layout.tsx b/app/(customer)/dashboard/layout.tsx index fcfcccf7..f8d25a67 100644 --- a/app/(customer)/dashboard/layout.tsx +++ b/app/(customer)/dashboard/layout.tsx @@ -8,21 +8,27 @@ import { currentUser } from "@/lib/auth/current-user"; import { ThemeMetaUpdater } from "@/features/theme/theme-meta-updater"; import { ModeToggle } from "@/features/theme/mode-toggle"; import { UpdateNotification } from "@/features/updates/update-notification"; +import {AclProvider} from "@/lib/acl/acl-context"; +import {env} from "@/env.mjs"; export default async function Layout({ children }: { children: ReactNode }) { const user = await currentUser(); if (!user) redirect("/login"); + const isDemoEnabled = env.DEMO_ENABLED + return ( -
- - } /> - -
} /> -
{children}
- -
+ +
+ + } /> + +
} /> +
{children}
+ +
+
); } diff --git a/src/features/profile/avatar-with-upload.tsx b/src/features/profile/avatar-with-upload.tsx index 5af93a53..d0a98ba2 100644 --- a/src/features/profile/avatar-with-upload.tsx +++ b/src/features/profile/avatar-with-upload.tsx @@ -1,32 +1,32 @@ "use client"; -import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar"; -import {UploadIcon} from "lucide-react"; -import {toast} from "sonner"; -import {uploadUserImageAction} from "@/features/upload/upload.action"; -import {useMutation} from "@tanstack/react-query"; -import {updateImageUserAction} from "@/features/profile/avatar.action"; -import {useRouter} from "next/navigation"; -import {User} from "@/db/schema/02_user"; -import React, {ChangeEvent} from "react"; + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { UploadIcon } from "lucide-react"; +import { toast } from "sonner"; +import { uploadUserImageAction } from "@/features/upload/upload.action"; +import { useMutation } from "@tanstack/react-query"; +import { updateImageUserAction } from "@/features/profile/avatar.action"; +import { useRouter } from "next/navigation"; +import { User } from "@/db/schema/02_user"; +import React, { ChangeEvent } from "react"; export type AvatarWithUploadProps = { user: User; + disabled?: boolean; }; -export const AvatarWithUpload = (props: AvatarWithUploadProps) => { - const user = props.user; +export const AvatarWithUpload = ({ user, disabled = false }: AvatarWithUploadProps) => { const router = useRouter(); const submitImage = useMutation({ mutationFn: async (file: File) => { const formData = new FormData(); formData.set("file", file); - const result = await uploadUserImageAction(formData); + const result = await uploadUserImageAction(formData); const inner = result?.data; if (inner?.success) { - const updateUser = await updateImageUserAction(inner.value ?? ""); const dataUser = updateUser?.data; @@ -40,12 +40,14 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => { } else { toast.error(inner?.actionError?.message); } - - }, }); + const isUploadDisabled = disabled || submitImage.isPending; + const handleImageUpload = async (event: ChangeEvent) => { + if (isUploadDisabled) return; + const file = event.target.files?.[0]; if (!file) return; @@ -57,6 +59,7 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => { } const maxSizeInMB = 5; + if (file.size > maxSizeInMB * 1024 * 1024) { toast.error(`Image must be smaller than ${maxSizeInMB}MB.`); return; @@ -65,29 +68,37 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => { submitImage.mutate(file); }; + const openFilePicker = () => { + if (isUploadDisabled) return; + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = ".jpg,.jpeg,.png,.webp"; + fileInput.disabled = isUploadDisabled; + + fileInput.onchange = (event: Event) => + handleImageUpload(event as unknown as React.ChangeEvent); + + fileInput.click(); + }; return ( -
- +
- - {user.name.charAt(0).toUpperCase()} + + + {user.name.charAt(0).toUpperCase()} + -
{ - const fileInput = document.createElement("input"); - fileInput.type = "file"; - fileInput.accept = ".jpg,.jpeg,.png,.webp"; - fileInput.onchange = (e: Event) => - handleImageUpload(e as unknown as React.ChangeEvent); - fileInput.click(); - }} - className="cursor-pointer absolute inset-0 flex justify-center items-center opacity-0 transition-opacity hover:opacity-30 hover:bg-gray-500 hover:bg-opacity-50 rounded-full w-24 h-24 lg:w-32 lg:h-32" - > - -
+ {!isUploadDisabled && ( +
+ +
+ )}
); -}; +}; \ No newline at end of file diff --git a/src/features/profile/profile-general.tsx b/src/features/profile/profile-general.tsx index 151a3169..bd53c52b 100644 --- a/src/features/profile/profile-general.tsx +++ b/src/features/profile/profile-general.tsx @@ -22,6 +22,7 @@ import {updateProfileSettingsAction} from "./profile.action"; import {User} from "@/db/schema/02_user"; import {ProfileSchema, ProfileSchemaType} from "./general.schema"; import {AvatarWithUpload} from "@/features/profile/avatar-with-upload"; +import {useAcl} from "@/lib/acl/acl-context"; interface ProfileGeneralProps { user: User; @@ -29,6 +30,7 @@ interface ProfileGeneralProps { export function ProfileGeneral({user}: ProfileGeneralProps) { const router = useRouter(); + const {isSuperAdminAndDemo} = useAcl() const profileForm = useZodForm({ schema: ProfileSchema, @@ -60,14 +62,15 @@ export function ProfileGeneral({user}: ProfileGeneralProps) {
-
+
-
updateProfile(values)}> + updateProfile(values)}>
{role} diff --git a/src/features/profile/profile-security.tsx b/src/features/profile/profile-security.tsx index 4ffcb910..8d04e3b9 100644 --- a/src/features/profile/profile-security.tsx +++ b/src/features/profile/profile-security.tsx @@ -45,6 +45,7 @@ import { Icon } from "@iconify/react"; import Image from "next/image"; import type { AuthProviderConfig } from "@/lib/auth/config"; import { is } from "date-fns/locale"; +import {useAcl} from "@/lib/acl/acl-context"; interface ProfileSecurityProps { user: User; @@ -66,6 +67,7 @@ export function ProfileSecurity({ providers, }: ProfileSecurityProps) { const router = useRouter(); + const {isSuperAdminAndDemo} = useAcl() const [isBackupCodesDialogOpen, setIsBackupCodesDialogOpen] = useState(false); const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false); diff --git a/src/features/profile/profile.action.ts b/src/features/profile/profile.action.ts index 8e7adfee..8d08d429 100644 --- a/src/features/profile/profile.action.ts +++ b/src/features/profile/profile.action.ts @@ -33,7 +33,7 @@ export const updateProfileSettingsAction = userAction.schema(UpdateProfileSchema await db .update(user) .set({ - ...(parsedInput.name ? { name: parsedInput.name } : {}), + ...(parsedInput.name ? { name: parsedInput.name.trim() } : {}), }) .where(eq(user.id, session.user.id)); diff --git a/src/lib/acl/acl-context.tsx b/src/lib/acl/acl-context.tsx new file mode 100644 index 00000000..87807f71 --- /dev/null +++ b/src/lib/acl/acl-context.tsx @@ -0,0 +1,33 @@ +"use client"; + +import {createContext, useContext, ReactNode} from "react"; +import {User} from "@/db/schema/02_user"; +import {useSystemPermissions} from "@/hooks/acl/use-system-acl"; +import {SystemPermissions} from "@/lib/acl/system-acl"; + + +type AclContextType = { + isDemoEnabled: boolean; + isSuperAdminAndDemo: boolean; + user: User; + permissions: SystemPermissions; +}; + +const AclContext = createContext(undefined); + +export const AclProvider = ({demo, user, children}: { demo: boolean, user:User, children: ReactNode }) => { + const permissions = useSystemPermissions(user); + const isSuperAdminAndDemo = demo && permissions.isSuperAdmin; + + return ( + + {children} + + ); +}; + +export const useAcl = () => { + const context = useContext(AclContext); + if (!context) throw new Error("useAcl must be used within AclProvider"); + return context; +}; diff --git a/src/lib/acl/system-acl.ts b/src/lib/acl/system-acl.ts index 85cc63ad..011a2a50 100644 --- a/src/lib/acl/system-acl.ts +++ b/src/lib/acl/system-acl.ts @@ -1,60 +1,60 @@ -import type { SystemRole } from "@/lib/acl/role"; +import type {SystemRole} from "@/lib/acl/role"; import {User} from "@/db/schema/02_user"; export type SystemPermissions = { - role: SystemRole | null; + role: SystemRole | null; - isSuperAdmin: boolean; - isAdmin: boolean; - isUser: boolean; + isSuperAdmin: boolean; + isAdmin: boolean; + isUser: boolean; + canAccessSystem: boolean; - canAccessSystem: boolean; + canCreateUser: boolean; + canUpdateUser: boolean; + canDeleteUser: boolean; - canCreateUser: boolean; - canUpdateUser: boolean; - canDeleteUser: boolean; + canAssignSuperAdmin: boolean; + canAssignAdmin: boolean; + canAssignUser: boolean; - canAssignSuperAdmin: boolean; - canAssignAdmin: boolean; - canAssignUser: boolean; + canCreateOrganization: boolean; + canDeleteOrganization: boolean; + canUpdateOrganization: boolean; - canCreateOrganization: boolean; - canDeleteOrganization: boolean; - canUpdateOrganization: boolean; - - canManageOrganizationUsers: boolean; + canManageOrganizationUsers: boolean; }; export const computeSystemPermissions = ( - user: User | null, + user: User | null, ): SystemPermissions => { - const role = (user?.role as SystemRole) ?? null; - const isSuperAdmin = role === "superadmin"; - const isAdmin = role === "admin"; - const isUser = role === "user"; + const role = (user?.role as SystemRole) ?? null; - return { - role, + const isSuperAdmin = role === "superadmin"; + const isAdmin = role === "admin"; + const isUser = role === "user"; - isSuperAdmin, - isAdmin, - isUser, + return { + role, - canAccessSystem: isSuperAdmin, + isSuperAdmin, + isAdmin, + isUser, - canCreateUser: isSuperAdmin || isAdmin, - canUpdateUser: isSuperAdmin || isAdmin, - canDeleteUser: isSuperAdmin || isAdmin, + canAccessSystem: isSuperAdmin, - canAssignSuperAdmin: isSuperAdmin, - canAssignAdmin: isSuperAdmin || isAdmin, - canAssignUser: isSuperAdmin || isAdmin, + canCreateUser: isSuperAdmin || isAdmin, + canUpdateUser: isSuperAdmin || isAdmin, + canDeleteUser: isSuperAdmin || isAdmin, - canCreateOrganization: isSuperAdmin, - canDeleteOrganization: isSuperAdmin, - canUpdateOrganization: isSuperAdmin || isAdmin, + canAssignSuperAdmin: isSuperAdmin, + canAssignAdmin: isSuperAdmin || isAdmin, + canAssignUser: isSuperAdmin || isAdmin, - canManageOrganizationUsers: isSuperAdmin || isAdmin, - }; + canCreateOrganization: isSuperAdmin, + canDeleteOrganization: isSuperAdmin, + canUpdateOrganization: isSuperAdmin || isAdmin, + + canManageOrganizationUsers: isSuperAdmin || isAdmin, + }; };