mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: demo acl
This commit is contained in:
@@ -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 (
|
||||
<SidebarProvider>
|
||||
<div className="flex flex-col lg:flex-row w-full">
|
||||
<ThemeMetaUpdater />
|
||||
<AppSidebar updateNotification={<UpdateNotification />} />
|
||||
<SidebarInset>
|
||||
<Header actions={<ModeToggle />} />
|
||||
<main className="h-full">{children}</main>
|
||||
</SidebarInset>
|
||||
</div>
|
||||
<AclProvider demo={isDemoEnabled} user={user}>
|
||||
<div className="flex flex-col lg:flex-row w-full">
|
||||
<ThemeMetaUpdater />
|
||||
<AppSidebar updateNotification={<UpdateNotification />} />
|
||||
<SidebarInset>
|
||||
<Header actions={<ModeToggle />} />
|
||||
<main className="h-full">{children}</main>
|
||||
</SidebarInset>
|
||||
</div>
|
||||
</AclProvider>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>);
|
||||
|
||||
fileInput.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative ">
|
||||
|
||||
<div className="relative">
|
||||
<Avatar className="w-24 h-24 lg:w-32 lg:h-32 border-4 border-muted/20">
|
||||
<AvatarImage className="object-cover" src={user.image || undefined}/>
|
||||
<AvatarFallback className="text-3xl">{user.name.charAt(0).toUpperCase()}</AvatarFallback>
|
||||
<AvatarImage className="object-cover" src={user.image || undefined} />
|
||||
<AvatarFallback className="text-3xl">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div
|
||||
onClick={() => {
|
||||
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<HTMLInputElement>);
|
||||
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"
|
||||
>
|
||||
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary"/>
|
||||
</div>
|
||||
{!isUploadDisabled && (
|
||||
<div
|
||||
onClick={openFilePicker}
|
||||
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"
|
||||
>
|
||||
<UploadIcon className="w-12 h-12 lg:w-16 lg:h-16 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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) {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-8 items-start">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex flex-col items-center gap-4" >
|
||||
<AvatarWithUpload
|
||||
disabled={isSuperAdminAndDemo}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 w-full max-w-lg">
|
||||
<Form form={profileForm} onSubmit={(values) => updateProfile(values)}>
|
||||
<Form disabled={isSuperAdminAndDemo} form={profileForm} onSubmit={(values) => updateProfile(values)}>
|
||||
<div className="space-y-6">
|
||||
<FormField
|
||||
control={profileForm.control}
|
||||
@@ -119,9 +122,7 @@ export function ProfileGeneral({user}: ProfileGeneralProps) {
|
||||
}
|
||||
|
||||
function RoleBadge({role}: { role: string }) {
|
||||
|
||||
const variant = role === "admin" ? "default" : "secondary";
|
||||
|
||||
return (
|
||||
<Badge variant={variant} className="capitalize px-2 py-0.5 text-xs">
|
||||
{role}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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<AclContextType | undefined>(undefined);
|
||||
|
||||
export const AclProvider = ({demo, user, children}: { demo: boolean, user:User, children: ReactNode }) => {
|
||||
const permissions = useSystemPermissions(user);
|
||||
const isSuperAdminAndDemo = demo && permissions.isSuperAdmin;
|
||||
|
||||
return (
|
||||
<AclContext.Provider value={{isDemoEnabled: demo,user, permissions, isSuperAdminAndDemo}}>
|
||||
{children}
|
||||
</AclContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAcl = () => {
|
||||
const context = useContext(AclContext);
|
||||
if (!context) throw new Error("useAcl must be used within AclProvider");
|
||||
return context;
|
||||
};
|
||||
+39
-39
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user