fix: block role change in org default settings

This commit is contained in:
charles-gauthereau
2026-06-10 21:54:46 +02:00
parent 545d19e42f
commit 8765e1cad2
7 changed files with 127 additions and 93 deletions
@@ -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>
+87 -80
View File
@@ -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>
);
+17
View File
@@ -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}>