Files
dokploy/packages/server/src/db/schema/sso.ts
Mauricio Siu 60f5ab304a feat(sso): enhance SAML provider registration and editing experience
- Added support for editing existing SAML providers, allowing users to update issuer, domains, entry point, and certificate.
- Introduced a new function to parse SAML configuration from JSON.
- Updated the UI to reflect changes in the registration dialog based on whether the user is adding or editing a provider.
- Improved user feedback with success messages tailored for registration and updates.
- Added a new column `created_at` to the `sso_provider` table for better tracking of provider creation times.
2026-02-12 23:49:27 -06:00

134 lines
4.1 KiB
TypeScript

import { relations } from "drizzle-orm";
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { z } from "zod";
import { organization } from "./account";
import { user } from "./user";
export const ssoProvider = pgTable("sso_provider", {
id: text("id").primaryKey(),
issuer: text("issuer").notNull(),
oidcConfig: text("oidc_config"),
samlConfig: text("saml_config"),
providerId: text("provider_id").notNull().unique(),
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
organizationId: text("organization_id").references(() => organization.id, {
onDelete: "cascade",
}),
domain: text("domain").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
});
export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({
organization: one(organization, {
fields: [ssoProvider.organizationId],
references: [organization.id],
}),
user: one(user, {
fields: [ssoProvider.userId],
references: [user.id],
}),
}));
const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/;
export const ssoProviderBodySchema = z.object({
providerId: z.string({}),
issuer: z.string({}),
domains: z
.string()
.array()
.transform((val) =>
Array.from(
new Set(val.map((d) => d.trim().toLowerCase()).filter(Boolean)),
),
)
.refine((val) => val.every((d) => domainRegex.test(d)), {
message: "Invalid domain",
path: ["domains"],
}),
oidcConfig: z
.object({
clientId: z.string({}),
clientSecret: z.string({}),
authorizationEndpoint: z.string({}).optional(),
tokenEndpoint: z.string({}).optional(),
userInfoEndpoint: z.string({}).optional(),
tokenEndpointAuthentication: z
.enum(["client_secret_post", "client_secret_basic"])
.optional(),
jwksEndpoint: z.string({}).optional(),
discoveryEndpoint: z.string().optional(),
skipDiscovery: z.boolean().optional(),
scopes: z.array(z.string()).optional(),
pkce: z.boolean().default(true).optional(),
mapping: z
.object({
id: z.string({}),
email: z.string({}),
emailVerified: z.string({}).optional(),
name: z.string({}),
image: z.string({}).optional(),
extraFields: z.record(z.string(), z.any()).optional(),
})
.optional(),
})
.optional(),
samlConfig: z
.object({
entryPoint: z.string({}),
cert: z.string({}),
callbackUrl: z.string({}),
audience: z.string().optional(),
idpMetadata: z
.object({
metadata: z.string().optional(),
entityID: z.string().optional(),
cert: z.string().optional(),
privateKey: z.string().optional(),
privateKeyPass: z.string().optional(),
isAssertionEncrypted: z.boolean().optional(),
encPrivateKey: z.string().optional(),
encPrivateKeyPass: z.string().optional(),
singleSignOnService: z
.array(
z.object({
Binding: z.string(),
Location: z.string(),
}),
)
.optional(),
})
.optional(),
spMetadata: z.object({
metadata: z.string().optional(),
entityID: z.string().optional(),
binding: z.string().optional(),
privateKey: z.string().optional(),
privateKeyPass: z.string().optional(),
isAssertionEncrypted: z.boolean().optional(),
encPrivateKey: z.string().optional(),
encPrivateKeyPass: z.string().optional(),
}),
wantAssertionsSigned: z.boolean().optional(),
authnRequestsSigned: z.boolean().optional(),
signatureAlgorithm: z.string().optional(),
digestAlgorithm: z.string().optional(),
identifierFormat: z.string().optional(),
privateKey: z.string().optional(),
decryptionPvk: z.string().optional(),
additionalParams: z.record(z.string(), z.any()).optional(),
mapping: z
.object({
id: z.string({}),
email: z.string({}),
emailVerified: z.string({}).optional(),
name: z.string({}),
firstName: z.string({}).optional(),
lastName: z.string({}).optional(),
extraFields: z.record(z.string(), z.any()).optional(),
})
.optional(),
})
.optional(),
organizationId: z.string({}).optional(),
overrideUserInfo: z.boolean({}).default(false).optional(),
});