mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: block role change in org default settings
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
OrganizationAddMemberModal
|
||||
} from "@/features/organizations/organization-add-member-modal";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {useAcl} from "@/lib/acl/acl-context";
|
||||
|
||||
type OrganizationManagementProps = {
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
@@ -26,6 +27,8 @@ type OrganizationManagementProps = {
|
||||
|
||||
export const OrganizationManagement = ({organization, users}: OrganizationManagementProps) => {
|
||||
|
||||
const {isDemoEnabled} = useAcl();
|
||||
const isDemoBlocked = isDemoEnabled && organization.slug === "default";
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [tab, setTab] = useState<string>(() => searchParams.get("tab") ?? "members");
|
||||
@@ -51,7 +54,7 @@ export const OrganizationManagement = ({organization, users}: OrganizationManage
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-3 md:mt-0">
|
||||
<OrganizationAddMemberModal organization={organization} users={users}/>
|
||||
<OrganizationAddMemberModal organization={organization} users={users} disabled={isDemoBlocked}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@@ -93,7 +96,7 @@ export const OrganizationManagement = ({organization, users}: OrganizationManage
|
||||
<div className="space-y-4">
|
||||
{organization.members.map((member: MemberWithUser) => (
|
||||
<OrganizationMemberCard key={member.id} member={member}
|
||||
organization={organization}/>
|
||||
organization={organization} disabled={isDemoBlocked}/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -106,7 +109,7 @@ export const OrganizationManagement = ({organization, users}: OrganizationManage
|
||||
<CardDescription>Organization configuration settings.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<UpdateOrganizationForm defaultValues={organization}/>
|
||||
<UpdateOrganizationForm defaultValues={organization} disabled={isDemoBlocked}/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { MemberWithUser } from "@/db/schema/03_organization";
|
||||
import { useState } from "react";
|
||||
import { authClient, useSession } from "@/lib/auth/auth-client";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||
import {useState} from "react";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -15,85 +15,92 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import {updateMemberRoleAction} from "@/features/organizations/update-member.action";
|
||||
import {RoleSchemaMember} from "@/features/organizations/member.schema";
|
||||
import {RestorationWith} from "@/db/schema/07_database";
|
||||
|
||||
export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: "Role",
|
||||
cell: ({ row }) => {
|
||||
const [role, setRole] = useState<string>(row.getValue("role"));
|
||||
const { data: session } = useSession();
|
||||
const activeOrgaMember = authClient.useActiveMember();
|
||||
export function organizationMemberColumns(
|
||||
isDemoBlocked: boolean,
|
||||
): ColumnDef<MemberWithUser>[] {
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateMemberRoleAction({
|
||||
memberId: row.original.id,
|
||||
organizationId: row.original.organizationId,
|
||||
role: RoleSchemaMember.parse(role),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success("User updated successfully.");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("An error occurred while updating user information.");
|
||||
},
|
||||
});
|
||||
return [
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: "Role",
|
||||
cell: ({row}) => {
|
||||
const [role, setRole] = useState<string>(row.getValue("role"));
|
||||
const {data: session} = useSession();
|
||||
const activeOrgaMember = authClient.useActiveMember();
|
||||
|
||||
// Only allow cycling between admin <-> member
|
||||
const handleUpdateRole = async () => {
|
||||
const nextRole = role === "admin" ? "member" : "admin";
|
||||
setRole(nextRole);
|
||||
await updateMutation.mutateAsync();
|
||||
};
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateMemberRoleAction({
|
||||
memberId: row.original.id,
|
||||
organizationId: row.original.organizationId,
|
||||
role: RoleSchemaMember.parse(role),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success("User updated successfully.");
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("An error occurred while updating user information.");
|
||||
},
|
||||
});
|
||||
|
||||
const isCurrentUser = session?.user.email === row.original.user.email;
|
||||
const isMember = activeOrgaMember.data?.role === "member";
|
||||
const isRowRoleOwner = role === "owner";
|
||||
// Only allow cycling between admin <-> member
|
||||
const handleUpdateRole = async () => {
|
||||
const nextRole = role === "admin" ? "member" : "admin";
|
||||
setRole(nextRole);
|
||||
await updateMutation.mutateAsync();
|
||||
};
|
||||
|
||||
const isDisabled = isMember || isCurrentUser || isRowRoleOwner;
|
||||
const isCurrentUser = session?.user.email === row.original.user.email;
|
||||
const isMember = activeOrgaMember.data?.role === "member";
|
||||
const isRowRoleOwner = role === "owner";
|
||||
|
||||
// Dynamic tooltip reason
|
||||
const disabledReason = isCurrentUser
|
||||
? "You cannot change your own role"
|
||||
: isRowRoleOwner
|
||||
? "Owner role cannot be modified"
|
||||
: "Members cannot edit roles";
|
||||
const isDisabled = isMember || isCurrentUser || isRowRoleOwner || isDemoBlocked;
|
||||
|
||||
const badge = (
|
||||
<Badge
|
||||
className={
|
||||
isDisabled
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: "cursor-pointer hover:bg-accent"
|
||||
}
|
||||
onClick={isDisabled ? undefined : handleUpdateRole}
|
||||
variant="outline"
|
||||
>
|
||||
{role}
|
||||
</Badge>
|
||||
);
|
||||
// Dynamic tooltip reason
|
||||
const disabledReason = isDemoBlocked
|
||||
? "Roles cannot be modified in the default organization in demo mode"
|
||||
: isCurrentUser
|
||||
? "You cannot change your own role"
|
||||
: isRowRoleOwner
|
||||
? "Owner role cannot be modified"
|
||||
: "Members cannot edit roles";
|
||||
|
||||
return isDisabled ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{badge}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{disabledReason}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
badge
|
||||
);
|
||||
const badge = (
|
||||
<Badge
|
||||
className={
|
||||
isDisabled
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: "cursor-pointer hover:bg-accent"
|
||||
}
|
||||
onClick={isDisabled ? undefined : handleUpdateRole}
|
||||
variant="outline"
|
||||
>
|
||||
{role}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
return isDisabled ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{badge}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{disabledReason}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
badge
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "user.name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "user.email",
|
||||
header: "Email",
|
||||
},
|
||||
];
|
||||
{
|
||||
accessorKey: "user.name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "user.email",
|
||||
header: "Email",
|
||||
},
|
||||
] };
|
||||
@@ -20,14 +20,15 @@ import {User} from "@/db/schema/02_user";
|
||||
type OrganizationAddMemberModalProps = {
|
||||
users: User[];
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const OrganizationAddMemberModal = ({users, organization}: OrganizationAddMemberModalProps) => {
|
||||
export const OrganizationAddMemberModal = ({users, organization, disabled}: OrganizationAddMemberModalProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<DialogTrigger asChild disabled={disabled}>
|
||||
<Button disabled={disabled}>
|
||||
<UserPlus className="w-4 h-4 mr-2"/>
|
||||
Add member
|
||||
</Button>
|
||||
|
||||
@@ -23,9 +23,10 @@ import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_or
|
||||
type OrganizationMemberCardProps = {
|
||||
member: MemberWithUser;
|
||||
organization: OrganizationWithMembersAndUsers;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const OrganizationMemberCard = ({member, organization}: OrganizationMemberCardProps) => {
|
||||
export const OrganizationMemberCard = ({member, organization, disabled}: OrganizationMemberCardProps) => {
|
||||
|
||||
const [isModalDeleteOpen, setIsModalDeleteOpen] = useState(false);
|
||||
const [isModalRoleOpen, setIsModalRoleOpen] = useState(false);
|
||||
@@ -62,8 +63,8 @@ export const OrganizationMemberCard = ({member, organization}: OrganizationMembe
|
||||
<div className="flex items-center space-x-2 mt-4 md:mt-0">
|
||||
<Badge variant={getRoleBadgeVariant(member.role)}>{member.role}</Badge>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<DropdownMenuTrigger asChild disabled={disabled}>
|
||||
<Button variant="ghost" size="icon" disabled={disabled}>
|
||||
<MoreHorizontal className="w-4 h-4"/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import {DataTable} from "@/components/common/data-table";
|
||||
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {
|
||||
organizationMemberColumns
|
||||
} from "@/features/organizations/member-columns";
|
||||
import {useAcl} from "@/lib/acl/acl-context";
|
||||
|
||||
interface SettingsOrganizationMembersTableProps {
|
||||
organization: OrganizationWithMembers
|
||||
}
|
||||
|
||||
export const SettingsOrganizationMembersTable = ({organization}: SettingsOrganizationMembersTableProps) => {
|
||||
const {isDemoEnabled} = useAcl();
|
||||
return (
|
||||
<div className="flex flex-col h-full ">
|
||||
<div className=" h-full">
|
||||
<DataTable
|
||||
columns={organizationMemberColumns}
|
||||
columns={organizationMemberColumns(isDemoEnabled && organization.slug === "default")}
|
||||
enableSelect={false}
|
||||
data={organization.members as MemberWithUser[]}/>
|
||||
</div>
|
||||
|
||||
@@ -18,9 +18,10 @@ import {updateOrganizationAction} from "@/features/organizations/organization.ac
|
||||
type UpdateOrganizationFormProps = {
|
||||
onSuccessAction?: () => void;
|
||||
defaultValues: OrganizationWithMembersAndUsers;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const UpdateOrganizationForm = ({onSuccessAction, defaultValues}: UpdateOrganizationFormProps) => {
|
||||
export const UpdateOrganizationForm = ({onSuccessAction, defaultValues, disabled}: UpdateOrganizationFormProps) => {
|
||||
|
||||
const router = useRouter();
|
||||
const {data: organizations, refetch} = authClient.useListOrganizations();
|
||||
@@ -30,7 +31,7 @@ export const UpdateOrganizationForm = ({onSuccessAction, defaultValues}: UpdateO
|
||||
const form = useZodForm({
|
||||
schema: UpdateOrganizationSchema,
|
||||
defaultValues: defaultValues,
|
||||
disabled: isDefaultOrganization,
|
||||
disabled: isDefaultOrganization || disabled,
|
||||
});
|
||||
|
||||
|
||||
@@ -83,7 +84,7 @@ export const UpdateOrganizationForm = ({onSuccessAction, defaultValues}: UpdateO
|
||||
/>
|
||||
|
||||
<div className="flex gap-4 justify-end">
|
||||
<ButtonWithLoading disabled={isDefaultOrganization} isPending={mutationUpdateOrganisation.isPending}>Validate</ButtonWithLoading>
|
||||
<ButtonWithLoading disabled={isDefaultOrganization || disabled} isPending={mutationUpdateOrganisation.isPending}>Validate</ButtonWithLoading>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
SettingsNotificationSection
|
||||
} from "@/features/settings/notification-section";
|
||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {useAcl} from "@/lib/acl/acl-context";
|
||||
import {Alert, AlertDescription} from "@/components/ui/alert";
|
||||
import {AlertTriangle} from "lucide-react";
|
||||
|
||||
export type SettingsTabsProps = {
|
||||
settings: Setting
|
||||
@@ -21,6 +24,7 @@ export type SettingsTabsProps = {
|
||||
};
|
||||
|
||||
export const SettingsTabs = ({settings, storageChannels, notificationChannels}: SettingsTabsProps) => {
|
||||
const {isDemoEnabled} = useAcl();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -67,6 +71,19 @@ export const SettingsTabs = ({settings, storageChannels, notificationChannels}:
|
||||
]
|
||||
|
||||
|
||||
if (isDemoEnabled) {
|
||||
return (
|
||||
<div className="h-full mt-3">
|
||||
<Alert variant="default">
|
||||
<AlertTriangle className="w-5 h-5 shrink-0 mt-0.5"/>
|
||||
<AlertDescription>
|
||||
System settings are not available in demo mode.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full mt-3">
|
||||
<Tabs className="h-full gap-4" value={tab} onValueChange={handleChangeTab}>
|
||||
|
||||
Reference in New Issue
Block a user