feat: add SQL migration for lucky echo and update foreign key constraints

- Introduced a new SQL migration file `0171_lucky_echo.sql` to modify the foreign key constraint on the `sso_provider` table, changing the `ON DELETE` behavior from `cascade` to `set null`.
- Updated the journal to include the new migration version and its associated tag.
- Added a snapshot file for version 7 of the database schema, reflecting the current state of the `sso_provider` and other related tables.

These changes enhance the integrity of the database by ensuring that user references are set to null instead of being deleted when the referenced user is removed.
This commit is contained in:
Mauricio Siu
2026-06-06 13:53:34 -06:00
parent 51b5af55d0
commit aa545ec71c
7 changed files with 8521 additions and 27 deletions

View File

@@ -0,0 +1,3 @@
ALTER TABLE "sso_provider" DROP CONSTRAINT "sso_provider_user_id_user_id_fk";
--> statement-breakpoint
ALTER TABLE "sso_provider" ADD CONSTRAINT "sso_provider_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;

File diff suppressed because it is too large Load Diff

View File

@@ -1198,6 +1198,13 @@
"when": 1780739532982, "when": 1780739532982,
"tag": "0170_amusing_spot", "tag": "0170_amusing_spot",
"breakpoints": true "breakpoints": true
},
{
"idx": 171,
"version": "7",
"when": 1780775037209,
"tag": "0171_lucky_echo",
"breakpoints": true
} }
] ]
} }

View File

@@ -19,13 +19,27 @@ import {
apiForwardAuthServerTarget, apiForwardAuthServerTarget,
apiSetForwardAuthSettings, apiSetForwardAuthSettings,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import { createTRPCRouter, enterpriseProcedure } from "@/server/api/trpc"; import { TRPCError } from "@trpc/server";
import {
createTRPCRouter,
enterpriseProcedure,
withPermission,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit"; import { audit } from "@/server/api/utils/audit";
export const forwardAuthRouter = createTRPCRouter({ export const forwardAuthRouter = createTRPCRouter({
getAuthDomain: enterpriseProcedure getAuthDomain: enterpriseProcedure
.input(apiForwardAuthServerTarget) .input(apiForwardAuthServerTarget)
.query(async ({ input }) => { .query(async ({ ctx, input }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",
});
}
}
const settings = await getForwardAuthSettings(input.serverId); const settings = await getForwardAuthSettings(input.serverId);
if (!settings) return null; if (!settings) return null;
return { return {
@@ -43,7 +57,15 @@ export const forwardAuthRouter = createTRPCRouter({
setAuthDomain: enterpriseProcedure setAuthDomain: enterpriseProcedure
.input(apiSetForwardAuthSettings) .input(apiSetForwardAuthSettings)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
if (input.serverId) await findServerById(input.serverId); if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",
});
}
}
const result = await setForwardAuthSettings({ const result = await setForwardAuthSettings({
organizationId: ctx.session.activeOrganizationId, organizationId: ctx.session.activeOrganizationId,
serverId: input.serverId, serverId: input.serverId,
@@ -64,7 +86,15 @@ export const forwardAuthRouter = createTRPCRouter({
removeAuthDomain: enterpriseProcedure removeAuthDomain: enterpriseProcedure
.input(apiForwardAuthServerTarget) .input(apiForwardAuthServerTarget)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
if (input.serverId) await findServerById(input.serverId); if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",
});
}
}
const result = await removeForwardAuthSettings(input.serverId); const result = await removeForwardAuthSettings(input.serverId);
await audit(ctx, { await audit(ctx, {
action: "delete", action: "delete",
@@ -76,10 +106,7 @@ export const forwardAuthRouter = createTRPCRouter({
}), }),
listProviders: enterpriseProcedure.query(({ ctx }) => listProviders: enterpriseProcedure.query(({ ctx }) =>
listSsoProvidersForOrg( listSsoProvidersForOrg(ctx.session.activeOrganizationId),
ctx.session.activeOrganizationId,
ctx.session.userId,
),
), ),
serverStatus: enterpriseProcedure.query(({ ctx }) => serverStatus: enterpriseProcedure.query(({ ctx }) =>
@@ -89,7 +116,15 @@ export const forwardAuthRouter = createTRPCRouter({
deployOnServer: enterpriseProcedure deployOnServer: enterpriseProcedure
.input(apiDeployForwardAuthOnServer) .input(apiDeployForwardAuthOnServer)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
if (input.serverId) await findServerById(input.serverId); if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",
});
}
}
const result = await deployForwardAuthOnServer({ const result = await deployForwardAuthOnServer({
serverId: input.serverId ?? undefined, serverId: input.serverId ?? undefined,
providerId: input.providerId, providerId: input.providerId,
@@ -107,7 +142,15 @@ export const forwardAuthRouter = createTRPCRouter({
removeOnServer: enterpriseProcedure removeOnServer: enterpriseProcedure
.input(apiForwardAuthServerTarget) .input(apiForwardAuthServerTarget)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
if (input.serverId) await findServerById(input.serverId); if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",
});
}
}
const result = await removeForwardAuthProxy(input.serverId); const result = await removeForwardAuthProxy(input.serverId);
await audit(ctx, { await audit(ctx, {
action: "delete", action: "delete",
@@ -118,11 +161,11 @@ export const forwardAuthRouter = createTRPCRouter({
return result; return result;
}), }),
status: enterpriseProcedure status: withPermission("domain", "read")
.input(apiForwardAuthDomainTarget) .input(apiForwardAuthDomainTarget)
.query(({ ctx, input }) => getDomainSsoStatus(ctx, input.domainId)), .query(({ ctx, input }) => getDomainSsoStatus(ctx, input.domainId)),
enable: enterpriseProcedure enable: withPermission("domain", "create")
.input(apiForwardAuthDomainTarget) .input(apiForwardAuthDomainTarget)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const domain = await assertApplicationDomainAccess( const domain = await assertApplicationDomainAccess(
@@ -142,7 +185,7 @@ export const forwardAuthRouter = createTRPCRouter({
return result; return result;
}), }),
disable: enterpriseProcedure disable: withPermission("domain", "create")
.input(apiForwardAuthDomainTarget) .input(apiForwardAuthDomainTarget)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const domain = await assertApplicationDomainAccess( const domain = await assertApplicationDomainAccess(

View File

@@ -53,10 +53,7 @@ export const ssoRouter = createTRPCRouter({
}), }),
listProviders: enterpriseProcedure.query(async ({ ctx }) => { listProviders: enterpriseProcedure.query(async ({ ctx }) => {
const providers = await db.query.ssoProvider.findMany({ const providers = await db.query.ssoProvider.findMany({
where: and( where: eq(ssoProvider.organizationId, ctx.session.activeOrganizationId),
eq(ssoProvider.organizationId, ctx.session.activeOrganizationId),
eq(ssoProvider.userId, ctx.session.userId),
),
columns: { columns: {
id: true, id: true,
providerId: true, providerId: true,
@@ -88,7 +85,6 @@ export const ssoRouter = createTRPCRouter({
where: and( where: and(
eq(ssoProvider.providerId, input.providerId), eq(ssoProvider.providerId, input.providerId),
eq(ssoProvider.organizationId, ctx.session.activeOrganizationId), eq(ssoProvider.organizationId, ctx.session.activeOrganizationId),
eq(ssoProvider.userId, ctx.session.userId),
), ),
columns: { columns: {
id: true, id: true,
@@ -116,12 +112,12 @@ export const ssoRouter = createTRPCRouter({
where: and( where: and(
eq(ssoProvider.providerId, input.providerId), eq(ssoProvider.providerId, input.providerId),
eq(ssoProvider.organizationId, ctx.session.activeOrganizationId), eq(ssoProvider.organizationId, ctx.session.activeOrganizationId),
eq(ssoProvider.userId, ctx.session.userId),
), ),
columns: { columns: {
id: true, id: true,
issuer: true, issuer: true,
domain: true, domain: true,
userId: true,
}, },
}); });
@@ -133,6 +129,13 @@ export const ssoRouter = createTRPCRouter({
}); });
} }
if (existing.userId !== ctx.session.userId) {
await db
.update(ssoProvider)
.set({ userId: ctx.session.userId })
.where(eq(ssoProvider.id, existing.id));
}
const providers = await db.query.ssoProvider.findMany({ const providers = await db.query.ssoProvider.findMany({
where: eq(ssoProvider.organizationId, ctx.session.activeOrganizationId), where: eq(ssoProvider.organizationId, ctx.session.activeOrganizationId),
columns: { providerId: true, domain: true }, columns: { providerId: true, domain: true },
@@ -218,7 +221,6 @@ export const ssoRouter = createTRPCRouter({
where: and( where: and(
eq(ssoProvider.providerId, input.providerId), eq(ssoProvider.providerId, input.providerId),
eq(ssoProvider.organizationId, ctx.session.activeOrganizationId), eq(ssoProvider.organizationId, ctx.session.activeOrganizationId),
eq(ssoProvider.userId, ctx.session.userId),
), ),
columns: { columns: {
id: true, id: true,
@@ -241,7 +243,6 @@ export const ssoRouter = createTRPCRouter({
and( and(
eq(ssoProvider.providerId, input.providerId), eq(ssoProvider.providerId, input.providerId),
eq(ssoProvider.organizationId, ctx.session.activeOrganizationId), eq(ssoProvider.organizationId, ctx.session.activeOrganizationId),
eq(ssoProvider.userId, ctx.session.userId),
), ),
) )
.returning({ id: ssoProvider.id }); .returning({ id: ssoProvider.id });

View File

@@ -10,7 +10,7 @@ export const ssoProvider = pgTable("sso_provider", {
oidcConfig: text("oidc_config"), oidcConfig: text("oidc_config"),
samlConfig: text("saml_config"), samlConfig: text("saml_config"),
providerId: text("provider_id").notNull().unique(), providerId: text("provider_id").notNull().unique(),
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }), userId: text("user_id").references(() => user.id, { onDelete: "set null" }),
organizationId: text("organization_id").references(() => organization.id, { organizationId: text("organization_id").references(() => organization.id, {
onDelete: "cascade", onDelete: "cascade",
}), }),

View File

@@ -84,14 +84,10 @@ const findProviderForOrg = async (
return provider; return provider;
}; };
export const listSsoProvidersForOrg = async ( export const listSsoProvidersForOrg = async (organizationId: string) => {
organizationId: string,
userId: string,
) => {
return db.query.ssoProvider.findMany({ return db.query.ssoProvider.findMany({
where: and( where: and(
eq(ssoProvider.organizationId, organizationId), eq(ssoProvider.organizationId, organizationId),
eq(ssoProvider.userId, userId),
isNotNull(ssoProvider.oidcConfig), isNotNull(ssoProvider.oidcConfig),
), ),
columns: { providerId: true, issuer: true, domain: true }, columns: { providerId: true, issuer: true, domain: true },