mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-14 11:06:15 +05:00
feat(organization): allow setting a default role for new members
This commit is contained in:
parent
1ccf15f331
commit
aaf8d7e12f
@ -106,6 +106,7 @@ export const AddInvitation = () => {
|
||||
const { mutateAsync: createUserWithCredentials, isPending: isCreating } =
|
||||
api.user.createUserWithCredentials.useMutation();
|
||||
const { data: customRoles } = api.customRole.all.useQuery();
|
||||
const { data: activeOrganization } = api.organization.active.useQuery();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const form = useForm<AddInvitation>({
|
||||
@ -132,6 +133,16 @@ export const AddInvitation = () => {
|
||||
}
|
||||
}, [form, isCloud]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
activeOrganization?.defaultRole &&
|
||||
activeOrganization.defaultRole !== "owner" &&
|
||||
!form.formState.dirtyFields.role
|
||||
) {
|
||||
form.setValue("role", activeOrganization.defaultRole);
|
||||
}
|
||||
}, [form, activeOrganization?.defaultRole]);
|
||||
|
||||
const onSubmit = async (data: AddInvitation) => {
|
||||
setError(null);
|
||||
|
||||
@ -267,10 +278,7 @@ export const AddInvitation = () => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a role" />
|
||||
|
||||
@ -45,6 +45,13 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
@ -780,6 +787,78 @@ function HandleCustomRole({
|
||||
);
|
||||
}
|
||||
|
||||
const DefaultRoleSection = ({ customRoles }: { customRoles: string[] }) => {
|
||||
const utils = api.useUtils();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: activeOrganization } = api.organization.active.useQuery();
|
||||
const { mutateAsync: updateOrganization, isPending: isUpdating } =
|
||||
api.organization.update.useMutation();
|
||||
const [selectedRole, setSelectedRole] = useState<string>();
|
||||
|
||||
if (auth?.role !== "owner" || !activeOrganization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentRole = activeOrganization.defaultRole ?? "member";
|
||||
const value = selectedRole ?? currentRole;
|
||||
|
||||
const onSave = async () => {
|
||||
await updateOrganization({
|
||||
organizationId: activeOrganization.id,
|
||||
name: activeOrganization.name,
|
||||
logo: activeOrganization.logo ?? undefined,
|
||||
defaultRole: value,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Default role updated");
|
||||
utils.organization.active.invalidate();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Error updating default role",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 rounded-lg border bg-muted/20 p-4">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Default role for new members</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Assigned automatically to users joining through SSO and preselected
|
||||
when creating invitations.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={value} onValueChange={setSelectedRole}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
{customRoles.map((role) => (
|
||||
<SelectItem key={role} value={role}>
|
||||
{role}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
isLoading={isUpdating}
|
||||
disabled={value === currentRole}
|
||||
onClick={onSave}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomRolesContent = () => {
|
||||
const {
|
||||
data: customRoles,
|
||||
@ -821,6 +900,9 @@ const CustomRolesContent = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<DefaultRoleSection
|
||||
customRoles={customRoles?.map((role) => role.role) ?? []}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<HandleCustomRole onSuccess={refetch} />
|
||||
</div>
|
||||
|
||||
1
apps/dokploy/drizzle/0182_skinny_wild_pack.sql
Normal file
1
apps/dokploy/drizzle/0182_skinny_wild_pack.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE "organization" ADD COLUMN "default_role" text;
|
||||
9037
apps/dokploy/drizzle/meta/0182_snapshot.json
Normal file
9037
apps/dokploy/drizzle/meta/0182_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1275,6 +1275,13 @@
|
||||
"when": 1786400712839,
|
||||
"tag": "0181_dashing_havok",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 182,
|
||||
"version": "7",
|
||||
"when": 1786435879438,
|
||||
"tag": "0182_skinny_wild_pack",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,5 +1,9 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { IS_CLOUD, sendInvitationEmail } from "@dokploy/server/index";
|
||||
import {
|
||||
hasValidLicense,
|
||||
IS_CLOUD,
|
||||
sendInvitationEmail,
|
||||
} from "@dokploy/server/index";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, exists } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
@ -128,6 +132,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
organizationId: z.string(),
|
||||
name: z.string(),
|
||||
logo: z.string().optional(),
|
||||
defaultRole: z.string().min(1).nullable().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@ -170,11 +175,47 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (input.defaultRole !== undefined && input.defaultRole !== null) {
|
||||
if (input.defaultRole === "owner") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Cannot set owner as the default role",
|
||||
});
|
||||
}
|
||||
|
||||
if (!["admin", "member"].includes(input.defaultRole)) {
|
||||
const customRole = await db.query.organizationRole.findFirst({
|
||||
where: and(
|
||||
eq(organizationRole.organizationId, input.organizationId),
|
||||
eq(organizationRole.role, input.defaultRole),
|
||||
),
|
||||
});
|
||||
|
||||
if (!customRole) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: `Role "${input.defaultRole}" not found`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasValidLicense(input.organizationId))) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Setting a custom role as default requires a valid enterprise license",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await db
|
||||
.update(organization)
|
||||
.set({
|
||||
name: input.name,
|
||||
logo: input.logo,
|
||||
...(input.defaultRole !== undefined && {
|
||||
defaultRole: input.defaultRole,
|
||||
}),
|
||||
})
|
||||
.where(eq(organization.id, input.organizationId))
|
||||
.returning();
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { member, organizationRole, user } from "@dokploy/server/db/schema";
|
||||
import {
|
||||
member,
|
||||
organization,
|
||||
organizationRole,
|
||||
user,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { statements } from "@dokploy/server/lib/access-control";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, count, eq } from "drizzle-orm";
|
||||
@ -255,6 +260,16 @@ export const customRoleRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await db
|
||||
.update(organization)
|
||||
.set({ defaultRole: null })
|
||||
.where(
|
||||
and(
|
||||
eq(organization.id, ctx.session.activeOrganizationId),
|
||||
eq(organization.defaultRole, input.roleName),
|
||||
),
|
||||
);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "customRole",
|
||||
|
||||
@ -66,6 +66,7 @@ export const organization = pgTable("organization", {
|
||||
logo: text("logo"),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
metadata: text("metadata"),
|
||||
defaultRole: text("default_role"),
|
||||
ownerId: text("owner_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
getUserByToken,
|
||||
} from "../services/admin";
|
||||
import { createAuditLog } from "../services/proprietary/audit-log";
|
||||
import { resolveOrganizationDefaultRole } from "../services/proprietary/license-key";
|
||||
import {
|
||||
getWebServerSettings,
|
||||
updateWebServerSettings,
|
||||
@ -289,10 +290,13 @@ const createBetterAuth = () =>
|
||||
message: "Provider not found",
|
||||
});
|
||||
}
|
||||
const defaultRole = provider.organizationId
|
||||
? await resolveOrganizationDefaultRole(provider.organizationId)
|
||||
: "member";
|
||||
await db.insert(schema.member).values({
|
||||
userId: user.id,
|
||||
organizationId: provider?.organizationId || "",
|
||||
role: "member",
|
||||
role: defaultRole,
|
||||
createdAt: new Date(),
|
||||
isDefault: true,
|
||||
});
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { user } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
organization,
|
||||
organizationRole,
|
||||
user,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { getOrganizationOwnerId } from "./sso";
|
||||
|
||||
export const hasValidLicense = async (organizationId: string) => {
|
||||
@ -22,3 +26,35 @@ export const hasValidLicense = async (organizationId: string) => {
|
||||
currentUser?.isValidEnterpriseLicense
|
||||
);
|
||||
};
|
||||
|
||||
export const resolveOrganizationDefaultRole = async (
|
||||
organizationId: string,
|
||||
) => {
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(organization.id, organizationId),
|
||||
columns: { defaultRole: true },
|
||||
});
|
||||
const defaultRole = org?.defaultRole;
|
||||
|
||||
if (!defaultRole || defaultRole === "owner") {
|
||||
return "member";
|
||||
}
|
||||
|
||||
if (defaultRole === "admin" || defaultRole === "member") {
|
||||
return defaultRole;
|
||||
}
|
||||
|
||||
const customRole = await db.query.organizationRole.findFirst({
|
||||
where: and(
|
||||
eq(organizationRole.organizationId, organizationId),
|
||||
eq(organizationRole.role, defaultRole),
|
||||
),
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
if (!customRole || !(await hasValidLicense(organizationId))) {
|
||||
return "member";
|
||||
}
|
||||
|
||||
return defaultRole;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user