mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-24 16:45:22 +02:00
feat: Add default organization selection (#1991)
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
PieChart,
|
||||
Server,
|
||||
ShieldCheck,
|
||||
Star,
|
||||
Trash2,
|
||||
User,
|
||||
Users,
|
||||
@@ -505,6 +506,8 @@ function SidebarLogo() {
|
||||
} = api.organization.all.useQuery();
|
||||
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
|
||||
api.organization.delete.useMutation();
|
||||
const { mutateAsync: setDefaultOrganization, isLoading: isSettingDefault } =
|
||||
api.organization.setDefault.useMutation();
|
||||
const { isMobile } = useSidebar();
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
const _utils = api.useUtils();
|
||||
@@ -605,7 +608,11 @@ function SidebarLogo() {
|
||||
}}
|
||||
className="w-full gap-2 p-2"
|
||||
>
|
||||
<div className="flex flex-col gap-4">{org.name}</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{org.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||
<Logo
|
||||
className={cn(
|
||||
@@ -618,6 +625,52 @@ function SidebarLogo() {
|
||||
</DropdownMenuItem>
|
||||
{org.ownerId === session?.user?.id && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"group",
|
||||
org.isDefault
|
||||
? "hover:bg-yellow-500/10"
|
||||
: "hover:bg-blue-500/10",
|
||||
)}
|
||||
isLoading={isSettingDefault && !org.isDefault}
|
||||
disabled={org.isDefault}
|
||||
onClick={async (e) => {
|
||||
if (org.isDefault) return;
|
||||
e.stopPropagation();
|
||||
await setDefaultOrganization({
|
||||
organizationId: org.id,
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
toast.success(
|
||||
"Default organization updated",
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(
|
||||
error?.message ||
|
||||
"Error setting default organization",
|
||||
);
|
||||
});
|
||||
}}
|
||||
title={org.isDefault ? "Default organization" : "Set as default"}
|
||||
>
|
||||
{org.isDefault ? (
|
||||
<Star
|
||||
fill="#eab308"
|
||||
stroke="#eab308"
|
||||
className="size-4 text-yellow-500"
|
||||
/>
|
||||
) : (
|
||||
<Star
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
className="size-4 text-gray-400 group-hover:text-blue-500 transition-colors"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
<AddOrganization organizationId={org.id} />
|
||||
<DialogAction
|
||||
title="Delete Organization"
|
||||
|
||||
1
apps/dokploy/drizzle/0114_sudden_sheva_callister.sql
Normal file
1
apps/dokploy/drizzle/0114_sudden_sheva_callister.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "member" ADD COLUMN "is_default" boolean DEFAULT false NOT NULL;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"id": "70e92aa8-56d1-4842-b844-775cb8198849",
|
||||
"id": "c615c052-2494-4e3c-9fc2-14f26fc65160",
|
||||
"prevId": "8310936d-e9ff-408b-ae6d-2292aac02523",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
@@ -421,6 +421,13 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_default": {
|
||||
"name": "is_default",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"canCreateProjects": {
|
||||
"name": "canCreateProjects",
|
||||
"type": "boolean",
|
||||
|
||||
@@ -834,6 +834,13 @@
|
||||
"when": 1761415824484,
|
||||
"tag": "0118_loose_anita_blake",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 119,
|
||||
"version": "7",
|
||||
"when": 1761920677980,
|
||||
"tag": "0114_sudden_sheva_callister",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -41,16 +41,90 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Check if this is the user's first organization
|
||||
const existingMemberships = await db.query.member.findMany({
|
||||
where: eq(member.userId, ctx.user.id),
|
||||
});
|
||||
|
||||
const isFirstOrganization = existingMemberships.length === 0;
|
||||
|
||||
await db.insert(member).values({
|
||||
organizationId: result.id,
|
||||
role: "owner",
|
||||
createdAt: new Date(),
|
||||
userId: ctx.user.id,
|
||||
isDefault: isFirstOrganization, // Mark as default if it's the first organization
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.organization.findMany({
|
||||
// Get all memberships for the user with organization info
|
||||
// Query memberships first to get the isDefault value correctly
|
||||
const memberships = await db
|
||||
.select({
|
||||
organizationId: member.organizationId,
|
||||
isDefault: member.isDefault,
|
||||
createdAt: member.createdAt,
|
||||
})
|
||||
.from(member)
|
||||
.where(eq(member.userId, ctx.user.id));
|
||||
|
||||
// If no default is set, set the oldest organization as default
|
||||
const hasDefault = memberships.some((m) => m.isDefault === true);
|
||||
if (!hasDefault && memberships.length > 0) {
|
||||
// Find the oldest membership (first created)
|
||||
const oldestMembership = memberships.reduce((oldest, current) =>
|
||||
current.createdAt < oldest.createdAt ? current : oldest,
|
||||
);
|
||||
|
||||
// Set it as default
|
||||
await db
|
||||
.update(member)
|
||||
.set({ isDefault: true })
|
||||
.where(
|
||||
and(
|
||||
eq(member.organizationId, oldestMembership.organizationId),
|
||||
eq(member.userId, ctx.user.id),
|
||||
),
|
||||
);
|
||||
|
||||
// Update the memberships array
|
||||
const updatedMemberships = memberships.map((m) =>
|
||||
m.organizationId === oldestMembership.organizationId
|
||||
? { ...m, isDefault: true }
|
||||
: m,
|
||||
);
|
||||
|
||||
// Get all organizations for the user
|
||||
const organizations = await db.query.organization.findMany({
|
||||
where: (organization) =>
|
||||
exists(
|
||||
db
|
||||
.select()
|
||||
.from(member)
|
||||
.where(
|
||||
and(
|
||||
eq(member.organizationId, organization.id),
|
||||
eq(member.userId, ctx.user.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
// Create a map of organizationId to isDefault
|
||||
const defaultMap = new Map(
|
||||
updatedMemberships.map((m) => [m.organizationId, Boolean(m.isDefault)]),
|
||||
);
|
||||
|
||||
// Map organizations with their isDefault flag
|
||||
return organizations.map((org) => ({
|
||||
...org,
|
||||
isDefault: defaultMap.get(org.id) ?? false,
|
||||
}));
|
||||
}
|
||||
|
||||
// Get all organizations for the user
|
||||
const organizations = await db.query.organization.findMany({
|
||||
where: (organization) =>
|
||||
exists(
|
||||
db
|
||||
@@ -64,7 +138,17 @@ export const organizationRouter = createTRPCRouter({
|
||||
),
|
||||
),
|
||||
});
|
||||
return memberResult;
|
||||
|
||||
// Create a map of organizationId to isDefault
|
||||
const defaultMap = new Map(
|
||||
memberships.map((m) => [m.organizationId, Boolean(m.isDefault)]),
|
||||
);
|
||||
|
||||
// Map organizations with their isDefault flag
|
||||
return organizations.map((org) => ({
|
||||
...org,
|
||||
isDefault: defaultMap.get(org.id) ?? false,
|
||||
}));
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.input(
|
||||
@@ -184,4 +268,45 @@ export const organizationRouter = createTRPCRouter({
|
||||
.delete(invitation)
|
||||
.where(eq(invitation.id, input.invitationId));
|
||||
}),
|
||||
setDefault: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
organizationId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Verify user is a member of this organization
|
||||
const userMember = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.organizationId, input.organizationId),
|
||||
eq(member.userId, ctx.user.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!userMember) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not a member of this organization",
|
||||
});
|
||||
}
|
||||
|
||||
// First, unset all defaults for this user
|
||||
await db
|
||||
.update(member)
|
||||
.set({ isDefault: false })
|
||||
.where(eq(member.userId, ctx.user.id));
|
||||
|
||||
// Then set this organization as default
|
||||
await db
|
||||
.update(member)
|
||||
.set({ isDefault: true })
|
||||
.where(
|
||||
and(
|
||||
eq(member.organizationId, input.organizationId),
|
||||
eq(member.userId, ctx.user.id),
|
||||
),
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -94,6 +94,7 @@ export const member = pgTable("member", {
|
||||
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
teamId: text("team_id"),
|
||||
isDefault: boolean("is_default").notNull().default(false),
|
||||
// Permissions
|
||||
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
||||
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
|
||||
|
||||
@@ -165,6 +165,7 @@ const { handler, api } = betterAuth({
|
||||
organizationId: organization?.id || "",
|
||||
role: "owner",
|
||||
createdAt: new Date(),
|
||||
isDefault: true, // Mark first organization as default
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -174,14 +175,28 @@ const { handler, api } = betterAuth({
|
||||
session: {
|
||||
create: {
|
||||
before: async (session) => {
|
||||
const member = await db.query.member.findFirst({
|
||||
where: eq(schema.member.userId, session.userId),
|
||||
orderBy: desc(schema.member.createdAt),
|
||||
// First try to find the default organization for this user
|
||||
let member = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(schema.member.userId, session.userId),
|
||||
eq(schema.member.isDefault, true),
|
||||
),
|
||||
with: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
// If no default is set, fallback to the most recently created organization
|
||||
if (!member) {
|
||||
member = await db.query.member.findFirst({
|
||||
where: eq(schema.member.userId, session.userId),
|
||||
orderBy: desc(schema.member.createdAt),
|
||||
with: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
...session,
|
||||
|
||||
Reference in New Issue
Block a user