mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
feat: implement SCIM provisioning support
- Updated dependencies for better-auth packages to version 1.6.5, including api-key, sso, and utils. - Introduced a new SCIM dialog component for managing SCIM providers and tokens. - Added SCIM provider management functionality in the backend, including listing, generating tokens, and deleting providers. - Created a new database table for SCIM providers and updated the schema accordingly. - Enhanced authentication logic to support SCIM provisioning with enterprise features validation.
This commit is contained in:
236
apps/dokploy/components/proprietary/sso/scim-dialog.tsx
Normal file
236
apps/dokploy/components/proprietary/sso/scim-dialog.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import { Copy, KeyRound, Loader2, Plus, Trash2 } from "lucide-react";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ScimDialog = ({ children }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const baseURL = useUrl();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [newProviderId, setNewProviderId] = useState("");
|
||||
const [justCreatedToken, setJustCreatedToken] = useState<{
|
||||
providerId: string;
|
||||
token: string;
|
||||
} | null>(null);
|
||||
|
||||
const { data: providers = [], isPending } = api.scim.listProviders.useQuery(
|
||||
undefined,
|
||||
{ enabled: open },
|
||||
);
|
||||
const { mutateAsync: generateToken, isPending: isGenerating } =
|
||||
api.scim.generateToken.useMutation();
|
||||
const { mutateAsync: deleteProvider, isPending: isDeleting } =
|
||||
api.scim.deleteProvider.useMutation();
|
||||
|
||||
const scimUrl = `${baseURL || "{baseURL}"}/api/auth/scim/v2`;
|
||||
|
||||
const handleGenerate = async () => {
|
||||
const providerId = newProviderId.trim().toLowerCase();
|
||||
if (!providerId) return;
|
||||
try {
|
||||
const result = await generateToken({ providerId });
|
||||
setJustCreatedToken({
|
||||
providerId: result.providerId,
|
||||
token: result.scimToken,
|
||||
});
|
||||
setNewProviderId("");
|
||||
await utils.scim.listProviders.invalidate();
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
err instanceof Error ? err.message : "Failed to generate SCIM token",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (providerId: string) => {
|
||||
try {
|
||||
await deleteProvider({ providerId });
|
||||
toast.success("SCIM provider removed");
|
||||
await utils.scim.listProviders.invalidate();
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
err instanceof Error ? err.message : "Failed to delete SCIM provider",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async (value: string, label: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
toast.success(`${label} copied`);
|
||||
} catch {
|
||||
toast.error("Failed to copy");
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenChange = (next: boolean) => {
|
||||
setOpen(next);
|
||||
if (!next) setJustCreatedToken(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[560px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<KeyRound className="size-5" />
|
||||
SCIM provisioning
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Automatically provision, update, and deactivate users from your
|
||||
identity provider (Okta, Entra ID, etc.). Configure the SCIM endpoint
|
||||
below in your IdP.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="grid gap-1">
|
||||
<Label className="text-xs font-medium text-muted-foreground">
|
||||
SCIM 2.0 endpoint URL
|
||||
</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="flex-1 break-all rounded-md bg-muted px-2 py-1.5 font-mono text-xs">
|
||||
{scimUrl}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8 shrink-0"
|
||||
onClick={() => handleCopy(scimUrl, "Endpoint URL")}
|
||||
disabled={!baseURL}
|
||||
>
|
||||
<Copy className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{justCreatedToken && (
|
||||
<div className="rounded-md border border-amber-500/40 bg-amber-500/10 p-3">
|
||||
<p className="text-sm font-medium">
|
||||
Bearer token for {justCreatedToken.providerId}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Copy this token now — it will not be shown again. Paste it into
|
||||
your IdP's SCIM configuration.
|
||||
</p>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<p className="flex-1 break-all rounded-md bg-background px-2 py-1.5 font-mono text-xs">
|
||||
{justCreatedToken.token}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8 shrink-0"
|
||||
onClick={() =>
|
||||
handleCopy(justCreatedToken.token, "Bearer token")
|
||||
}
|
||||
>
|
||||
<Copy className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">
|
||||
Generate token for a new provider
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={newProviderId}
|
||||
onChange={(e) => setNewProviderId(e.target.value)}
|
||||
placeholder="okta, entra, jumpcloud..."
|
||||
className="font-mono text-sm"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
void handleGenerate();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleGenerate}
|
||||
disabled={!newProviderId.trim() || isGenerating}
|
||||
>
|
||||
<Plus className="mr-1 size-4" />
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose a unique identifier for this IdP connection (lowercase,
|
||||
alphanumeric, dashes).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Existing providers</Label>
|
||||
{isPending ? (
|
||||
<div className="flex items-center gap-2 justify-center py-4">
|
||||
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">Loading...</span>
|
||||
</div>
|
||||
) : providers.length === 0 ? (
|
||||
<p className="rounded-md border border-dashed bg-muted/30 px-3 py-4 text-center text-sm text-muted-foreground">
|
||||
No SCIM providers configured yet.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-2">
|
||||
{providers.map((provider) => (
|
||||
<li
|
||||
key={provider.id}
|
||||
className="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2"
|
||||
>
|
||||
<span className="flex-1 font-mono text-sm">
|
||||
{provider.providerId}
|
||||
</span>
|
||||
<DialogAction
|
||||
title="Remove SCIM provider"
|
||||
description={`Remove "${provider.providerId}"? Existing provisioned users will stay but the IdP will no longer be able to sync.`}
|
||||
type="destructive"
|
||||
onClick={() => handleDelete(provider.providerId)}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 shrink-0 text-destructive hover:text-destructive"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import {
|
||||
Eye,
|
||||
KeyRound,
|
||||
Loader2,
|
||||
LogIn,
|
||||
Pencil,
|
||||
@@ -34,6 +35,7 @@ import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { RegisterOidcDialog } from "./register-oidc-dialog";
|
||||
import { RegisterSamlDialog } from "./register-saml-dialog";
|
||||
import { ScimDialog } from "./scim-dialog";
|
||||
|
||||
type ProviderForDetails = {
|
||||
id: string | null;
|
||||
@@ -169,15 +171,22 @@ export const SSOSettings = () => {
|
||||
Users can sign in with their organization's IdP.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setManageOriginsOpen(true)}
|
||||
className="shrink-0"
|
||||
>
|
||||
<Shield className="mr-2 size-4" />
|
||||
Manage origins
|
||||
</Button>
|
||||
<div className="flex flex-wrap gap-2 shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setManageOriginsOpen(true)}
|
||||
>
|
||||
<Shield className="mr-2 size-4" />
|
||||
Manage origins
|
||||
</Button>
|
||||
<ScimDialog>
|
||||
<Button variant="outline" size="sm">
|
||||
<KeyRound className="mr-2 size-4" />
|
||||
Manage SCIM
|
||||
</Button>
|
||||
</ScimDialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isPending ? (
|
||||
|
||||
11
apps/dokploy/drizzle/0166_overjoyed_big_bertha.sql
Normal file
11
apps/dokploy/drizzle/0166_overjoyed_big_bertha.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE "scim_provider" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"provider_id" text NOT NULL,
|
||||
"scim_token" text NOT NULL,
|
||||
"organization_id" text,
|
||||
CONSTRAINT "scim_provider_provider_id_unique" UNIQUE("provider_id"),
|
||||
CONSTRAINT "scim_provider_scim_token_unique" UNIQUE("scim_token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "two_factor" ADD COLUMN "verified" boolean DEFAULT true NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "scim_provider" ADD CONSTRAINT "scim_provider_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;
|
||||
8385
apps/dokploy/drizzle/meta/0166_snapshot.json
Normal file
8385
apps/dokploy/drizzle/meta/0166_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1163,6 +1163,13 @@
|
||||
"when": 1775845419261,
|
||||
"tag": "0165_abnormal_greymalkin",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 166,
|
||||
"version": "7",
|
||||
"when": 1776576422440,
|
||||
"tag": "0166_overjoyed_big_bertha",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -46,8 +46,8 @@
|
||||
"@ai-sdk/mistral": "^3.0.20",
|
||||
"@ai-sdk/openai": "^3.0.29",
|
||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||
"@better-auth/api-key": "1.5.4",
|
||||
"@better-auth/sso": "1.5.4",
|
||||
"@better-auth/api-key": "1.6.5",
|
||||
"@better-auth/sso": "1.6.5",
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
@@ -101,7 +101,7 @@
|
||||
"ai": "^6.0.86",
|
||||
"ai-sdk-ollama": "^3.7.0",
|
||||
"bcrypt": "5.1.1",
|
||||
"better-auth": "1.5.4",
|
||||
"better-auth": "1.6.5",
|
||||
"bl": "6.0.11",
|
||||
"boxen": "^7.1.1",
|
||||
"bullmq": "5.67.3",
|
||||
@@ -113,7 +113,7 @@
|
||||
"dockerode": "4.0.2",
|
||||
"dompurify": "^3.3.3",
|
||||
"dotenv": "16.4.5",
|
||||
"drizzle-orm": "0.45.1",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"drizzle-zod": "0.8.3",
|
||||
"fancy-ansi": "^0.1.3",
|
||||
"input-otp": "^1.4.2",
|
||||
|
||||
@@ -31,6 +31,7 @@ import { projectRouter } from "./routers/project";
|
||||
import { auditLogRouter } from "./routers/proprietary/audit-log";
|
||||
import { customRoleRouter } from "./routers/proprietary/custom-role";
|
||||
import { licenseKeyRouter } from "./routers/proprietary/license-key";
|
||||
import { scimRouter } from "./routers/proprietary/scim";
|
||||
import { ssoRouter } from "./routers/proprietary/sso";
|
||||
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
|
||||
import { redirectsRouter } from "./routers/redirects";
|
||||
@@ -93,6 +94,7 @@ export const appRouter = createTRPCRouter({
|
||||
organization: organizationRouter,
|
||||
licenseKey: licenseKeyRouter,
|
||||
sso: ssoRouter,
|
||||
scim: scimRouter,
|
||||
whitelabeling: whitelabelingRouter,
|
||||
customRole: customRoleRouter,
|
||||
auditLog: auditLogRouter,
|
||||
|
||||
78
apps/dokploy/server/api/routers/proprietary/scim.ts
Normal file
78
apps/dokploy/server/api/routers/proprietary/scim.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { scimProvider } from "@dokploy/server/db/schema";
|
||||
import { requestToHeaders } from "@dokploy/server/index";
|
||||
import { auth } from "@dokploy/server/lib/auth";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, asc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, enterpriseProcedure } from "@/server/api/trpc";
|
||||
|
||||
const providerIdSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(
|
||||
/^[a-z0-9][a-z0-9-]*$/,
|
||||
"Provider ID must be lowercase alphanumeric with optional dashes",
|
||||
);
|
||||
|
||||
export const scimRouter = createTRPCRouter({
|
||||
listProviders: enterpriseProcedure.query(async ({ ctx }) => {
|
||||
const providers = await db.query.scimProvider.findMany({
|
||||
where: eq(scimProvider.organizationId, ctx.session.activeOrganizationId),
|
||||
columns: {
|
||||
id: true,
|
||||
providerId: true,
|
||||
organizationId: true,
|
||||
},
|
||||
orderBy: [asc(scimProvider.providerId)],
|
||||
});
|
||||
return providers;
|
||||
}),
|
||||
generateToken: enterpriseProcedure
|
||||
.input(z.object({ providerId: providerIdSchema }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const existing = await db.query.scimProvider.findFirst({
|
||||
where: eq(scimProvider.providerId, input.providerId),
|
||||
columns: { id: true, organizationId: true },
|
||||
});
|
||||
if (existing) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "A SCIM provider with this ID already exists",
|
||||
});
|
||||
}
|
||||
const result = await auth.generateSCIMToken({
|
||||
body: {
|
||||
providerId: input.providerId,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
},
|
||||
headers: requestToHeaders(ctx.req),
|
||||
});
|
||||
return { scimToken: result.scimToken, providerId: input.providerId };
|
||||
}),
|
||||
deleteProvider: enterpriseProcedure
|
||||
.input(z.object({ providerId: providerIdSchema }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const [deleted] = await db
|
||||
.delete(scimProvider)
|
||||
.where(
|
||||
and(
|
||||
eq(scimProvider.providerId, input.providerId),
|
||||
eq(
|
||||
scimProvider.organizationId,
|
||||
ctx.session.activeOrganizationId,
|
||||
),
|
||||
),
|
||||
)
|
||||
.returning({ id: scimProvider.id });
|
||||
if (!deleted) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message:
|
||||
"SCIM provider not found or you do not have permission to delete it",
|
||||
});
|
||||
}
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
@@ -37,9 +37,10 @@
|
||||
"@ai-sdk/mistral": "^3.0.20",
|
||||
"@ai-sdk/openai": "^3.0.29",
|
||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||
"@better-auth/api-key": "1.5.4",
|
||||
"@better-auth/sso": "1.5.4",
|
||||
"@better-auth/utils": "0.3.1",
|
||||
"@better-auth/api-key": "1.6.5",
|
||||
"@better-auth/scim": "^1.6.5",
|
||||
"@better-auth/sso": "1.6.5",
|
||||
"@better-auth/utils": "0.4.0",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@octokit/auth-app": "^6.1.3",
|
||||
"@octokit/rest": "^20.1.2",
|
||||
@@ -51,15 +52,14 @@
|
||||
"ai": "^6.0.86",
|
||||
"ai-sdk-ollama": "^3.7.0",
|
||||
"bcrypt": "5.1.1",
|
||||
"better-auth": "1.5.4",
|
||||
"better-call": "2.0.2",
|
||||
"better-auth": "1.6.5",
|
||||
"bl": "6.0.11",
|
||||
"boxen": "^7.1.1",
|
||||
"date-fns": "3.6.0",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "16.4.5",
|
||||
"drizzle-dbml-generator": "0.10.0",
|
||||
"drizzle-orm": "0.45.1",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"drizzle-zod": "0.5.1",
|
||||
"lodash": "4.17.21",
|
||||
"micromatch": "4.0.8",
|
||||
|
||||
@@ -214,6 +214,7 @@ export const twoFactor = pgTable("two_factor", {
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
verified: boolean("verified").notNull().default(true),
|
||||
});
|
||||
|
||||
export const apikey = pgTable("apikey", {
|
||||
|
||||
@@ -30,6 +30,7 @@ export * from "./redis";
|
||||
export * from "./registry";
|
||||
export * from "./rollbacks";
|
||||
export * from "./schedule";
|
||||
export * from "./scim";
|
||||
export * from "./security";
|
||||
export * from "./server";
|
||||
export * from "./session";
|
||||
|
||||
22
packages/server/src/db/schema/scim.ts
Normal file
22
packages/server/src/db/schema/scim.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { organization } from "./account";
|
||||
|
||||
export const scimProvider = pgTable("scim_provider", {
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
providerId: text("provider_id").notNull().unique(),
|
||||
scimToken: text("scim_token").notNull().unique(),
|
||||
organizationId: text("organization_id").references(() => organization.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const scimProviderRelations = relations(scimProvider, ({ one }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [scimProvider.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
}));
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { apiKey } from "@better-auth/api-key";
|
||||
import { scim } from "@better-auth/scim";
|
||||
import { sso } from "@better-auth/sso";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { betterAuth } from "better-auth";
|
||||
@@ -178,7 +179,8 @@ const { handler, api } = betterAuth({
|
||||
}
|
||||
} else {
|
||||
const isSSORequest = context?.path.includes("/sso");
|
||||
if (isSSORequest) {
|
||||
const isSCIMRequest = context?.path.includes("/scim");
|
||||
if (isSSORequest || isSCIMRequest) {
|
||||
return;
|
||||
}
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
@@ -194,6 +196,7 @@ const { handler, api } = betterAuth({
|
||||
},
|
||||
after: async (user, context) => {
|
||||
const isSSORequest = context?.path.includes("/sso");
|
||||
const isSCIMRequest = context?.path.includes("/scim");
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
});
|
||||
@@ -229,6 +232,10 @@ const { handler, api } = betterAuth({
|
||||
}
|
||||
}
|
||||
|
||||
if (isSCIMRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (IS_CLOUD || !isAdminPresent) {
|
||||
await db.transaction(async (tx) => {
|
||||
const organization = await tx
|
||||
@@ -396,7 +403,24 @@ const { handler, api } = betterAuth({
|
||||
enableMetadata: true,
|
||||
references: "user",
|
||||
}),
|
||||
sso(),
|
||||
sso({
|
||||
saml: {
|
||||
enableInResponseToValidation: false,
|
||||
},
|
||||
}),
|
||||
scim({
|
||||
beforeSCIMTokenGenerated: async ({ user }) => {
|
||||
const dbUser = await db.query.user.findFirst({
|
||||
where: eq(schema.user.id, user.id),
|
||||
columns: { enableEnterpriseFeatures: true },
|
||||
});
|
||||
if (!dbUser?.enableEnterpriseFeatures) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "SCIM provisioning requires an enterprise license",
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
twoFactor(),
|
||||
organization({
|
||||
ac,
|
||||
@@ -442,6 +466,9 @@ const _auth = {
|
||||
createApiKey: api.createApiKey,
|
||||
registerSSOProvider: api.registerSSOProvider,
|
||||
updateSSOProvider: api.updateSSOProvider,
|
||||
generateSCIMToken: api.generateSCIMToken,
|
||||
listSCIMProviderConnections: api.listSCIMProviderConnections,
|
||||
deleteSCIMProviderConnection: api.deleteSCIMProviderConnection,
|
||||
};
|
||||
|
||||
export type AuthType = typeof _auth;
|
||||
|
||||
763
pnpm-lock.yaml
generated
763
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user