mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge pull request #3700 from Dokploy/feat/edit-sso-providers
Feat/edit sso providers
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { FieldArrayPath } from "react-hook-form";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
|
||||
const DEFAULT_SCOPES = ["openid", "email", "profile"];
|
||||
|
||||
@@ -58,6 +59,7 @@ const oidcProviderSchema = z.object({
|
||||
type OidcProviderForm = z.infer<typeof oidcProviderSchema>;
|
||||
|
||||
interface RegisterOidcDialogProps {
|
||||
providerId?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -70,16 +72,86 @@ const formDefaultValues = {
|
||||
scopes: [...DEFAULT_SCOPES],
|
||||
};
|
||||
|
||||
export function RegisterOidcDialog({ children }: RegisterOidcDialogProps) {
|
||||
function parseOidcConfig(oidcConfig: string | null): {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
scopes?: string[];
|
||||
} | null {
|
||||
if (!oidcConfig) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(oidcConfig) as {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
scopes?: string[];
|
||||
};
|
||||
return {
|
||||
clientId: parsed.clientId,
|
||||
clientSecret: parsed.clientSecret,
|
||||
scopes: Array.isArray(parsed.scopes) ? parsed.scopes : undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function RegisterOidcDialog({
|
||||
providerId,
|
||||
children,
|
||||
}: RegisterOidcDialogProps) {
|
||||
const utils = api.useUtils();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { mutateAsync, isLoading } = api.sso.register.useMutation();
|
||||
|
||||
const { data } = api.sso.one.useQuery(
|
||||
{ providerId: providerId ?? "" },
|
||||
{ enabled: !!providerId && open },
|
||||
);
|
||||
const registerMutation = api.sso.register.useMutation();
|
||||
const updateMutation = api.sso.update.useMutation();
|
||||
|
||||
const isEdit = !!providerId;
|
||||
const mutateAsync = isEdit
|
||||
? updateMutation.mutateAsync
|
||||
: registerMutation.mutateAsync;
|
||||
const isLoading = isEdit
|
||||
? updateMutation.isLoading
|
||||
: registerMutation.isLoading;
|
||||
|
||||
const form = useForm<OidcProviderForm>({
|
||||
resolver: zodResolver(oidcProviderSchema),
|
||||
defaultValues: formDefaultValues,
|
||||
});
|
||||
|
||||
const watchedProviderId = useWatch({
|
||||
control: form.control,
|
||||
name: "providerId",
|
||||
defaultValue: "",
|
||||
});
|
||||
|
||||
const baseURL = useUrl();
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !open) return;
|
||||
const domains = data.domain
|
||||
? data.domain
|
||||
.split(",")
|
||||
.map((d) => d.trim())
|
||||
.filter(Boolean)
|
||||
: [""];
|
||||
if (domains.length === 0) domains.push("");
|
||||
const oidc = parseOidcConfig(data.oidcConfig);
|
||||
form.reset({
|
||||
providerId: data.providerId,
|
||||
issuer: data.issuer,
|
||||
domains,
|
||||
clientId: oidc?.clientId ?? "",
|
||||
clientSecret: oidc?.clientSecret ?? "",
|
||||
scopes:
|
||||
oidc?.scopes && oidc.scopes.length > 0
|
||||
? oidc.scopes
|
||||
: [...DEFAULT_SCOPES],
|
||||
});
|
||||
}, [data, open, form]);
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "domains" as FieldArrayPath<OidcProviderForm>,
|
||||
@@ -130,7 +202,11 @@ export function RegisterOidcDialog({ children }: RegisterOidcDialogProps) {
|
||||
},
|
||||
});
|
||||
|
||||
toast.success("OIDC provider registered successfully");
|
||||
toast.success(
|
||||
isEdit
|
||||
? "OIDC provider updated successfully"
|
||||
: "OIDC provider registered successfully",
|
||||
);
|
||||
form.reset(formDefaultValues);
|
||||
setOpen(false);
|
||||
await utils.sso.listProviders.invalidate();
|
||||
@@ -146,11 +222,13 @@ export function RegisterOidcDialog({ children }: RegisterOidcDialogProps) {
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Register OIDC provider</DialogTitle>
|
||||
<DialogTitle>
|
||||
{isEdit ? "Update OIDC provider" : "Register OIDC provider"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add any OIDC-compliant identity provider (e.g. Okta, Azure AD,
|
||||
Google Workspace, Auth0, Keycloak). Discovery will fill endpoints
|
||||
from the issuer URL when possible.
|
||||
{isEdit
|
||||
? "Change issuer, domains, client settings or scopes. Provider ID cannot be changed."
|
||||
: "Add any OIDC-compliant identity provider (e.g. Okta, Azure AD, Google Workspace, Auth0, Keycloak). Discovery will fill endpoints from the issuer URL when possible."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
@@ -162,11 +240,28 @@ export function RegisterOidcDialog({ children }: RegisterOidcDialogProps) {
|
||||
<FormItem>
|
||||
<FormLabel>Provider ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="e.g. okta or my-idp" {...field} />
|
||||
<Input
|
||||
placeholder="e.g. okta or my-idp"
|
||||
{...field}
|
||||
readOnly={isEdit}
|
||||
className={isEdit ? "bg-muted" : undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Unique identifier; used in callback URL path.
|
||||
{isEdit && " Cannot be changed when editing."}
|
||||
</FormDescription>
|
||||
{baseURL && (
|
||||
<div className="rounded-md bg-muted px-3 py-2 text-xs">
|
||||
<p className="font-medium text-muted-foreground">
|
||||
Callback URL (configure in your IdP)
|
||||
</p>
|
||||
<p className="mt-0.5 break-all font-mono">
|
||||
{baseURL}/api/auth/sso/callback/
|
||||
{watchedProviderId?.trim() || "..."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -341,7 +436,7 @@ export function RegisterOidcDialog({ children }: RegisterOidcDialogProps) {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" isLoading={isLoading}>
|
||||
Register provider
|
||||
{isEdit ? "Update provider" : "Register provider"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { type FieldArrayPath, useFieldArray, useForm } from "react-hook-form";
|
||||
import {
|
||||
type FieldArrayPath,
|
||||
useFieldArray,
|
||||
useForm,
|
||||
useWatch,
|
||||
} from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -28,6 +33,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
|
||||
const domainsArraySchema = z
|
||||
.array(z.string().trim())
|
||||
@@ -58,6 +64,7 @@ const samlProviderSchema = z.object({
|
||||
type SamlProviderForm = z.infer<typeof samlProviderSchema>;
|
||||
|
||||
interface RegisterSamlDialogProps {
|
||||
providerId?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -70,24 +77,83 @@ const formDefaultValues: SamlProviderForm = {
|
||||
idpMetadataXml: "",
|
||||
};
|
||||
|
||||
export function RegisterSamlDialog({ children }: RegisterSamlDialogProps) {
|
||||
function parseSamlConfig(samlConfig: string | null): {
|
||||
entryPoint?: string;
|
||||
cert?: string;
|
||||
idpMetadataXml?: string;
|
||||
} | null {
|
||||
if (!samlConfig) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(samlConfig) as {
|
||||
entryPoint?: string;
|
||||
cert?: string;
|
||||
idpMetadata?: { metadata?: string };
|
||||
};
|
||||
return {
|
||||
entryPoint: parsed.entryPoint,
|
||||
cert: parsed.cert,
|
||||
idpMetadataXml: parsed.idpMetadata?.metadata,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function RegisterSamlDialog({
|
||||
providerId,
|
||||
children,
|
||||
}: RegisterSamlDialogProps) {
|
||||
const utils = api.useUtils();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { mutateAsync, isLoading } = api.sso.register.useMutation();
|
||||
|
||||
const [baseURL, setBaseURL] = useState("");
|
||||
const { data } = api.sso.one.useQuery(
|
||||
{ providerId: providerId ?? "" },
|
||||
{ enabled: !!providerId && open },
|
||||
);
|
||||
const registerMutation = api.sso.register.useMutation();
|
||||
const updateMutation = api.sso.update.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
setBaseURL(window.location.origin);
|
||||
}
|
||||
}, []);
|
||||
const isEdit = !!providerId;
|
||||
const mutateAsync = isEdit
|
||||
? updateMutation.mutateAsync
|
||||
: registerMutation.mutateAsync;
|
||||
const isLoading = isEdit
|
||||
? updateMutation.isLoading
|
||||
: registerMutation.isLoading;
|
||||
|
||||
const baseURL = useUrl();
|
||||
|
||||
const form = useForm<SamlProviderForm>({
|
||||
resolver: zodResolver(samlProviderSchema),
|
||||
defaultValues: formDefaultValues,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !open) return;
|
||||
const domains = data.domain
|
||||
? data.domain
|
||||
.split(",")
|
||||
.map((d) => d.trim())
|
||||
.filter(Boolean)
|
||||
: [""];
|
||||
if (domains.length === 0) domains.push("");
|
||||
const saml = parseSamlConfig(data.samlConfig);
|
||||
form.reset({
|
||||
providerId: data.providerId,
|
||||
issuer: data.issuer,
|
||||
domains,
|
||||
entryPoint: saml?.entryPoint ?? "",
|
||||
cert: saml?.cert ?? "",
|
||||
idpMetadataXml: saml?.idpMetadataXml ?? "",
|
||||
});
|
||||
}, [data, open, form]);
|
||||
|
||||
const watchedProviderId = useWatch({
|
||||
control: form.control,
|
||||
name: "providerId",
|
||||
defaultValue: "",
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "domains" as FieldArrayPath<SamlProviderForm>,
|
||||
@@ -133,7 +199,11 @@ export function RegisterSamlDialog({ children }: RegisterSamlDialogProps) {
|
||||
},
|
||||
});
|
||||
|
||||
toast.success("SAML provider registered successfully");
|
||||
toast.success(
|
||||
isEdit
|
||||
? "SAML provider updated successfully"
|
||||
: "SAML provider registered successfully",
|
||||
);
|
||||
form.reset(formDefaultValues);
|
||||
setOpen(false);
|
||||
await utils.sso.listProviders.invalidate();
|
||||
@@ -149,10 +219,13 @@ export function RegisterSamlDialog({ children }: RegisterSamlDialogProps) {
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Register SAML provider</DialogTitle>
|
||||
<DialogTitle>
|
||||
{isEdit ? "Update SAML provider" : "Register SAML provider"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a SAML 2.0 identity provider (e.g. Okta SAML, Azure AD SAML,
|
||||
OneLogin). You need the IdP's SSO URL and signing certificate.
|
||||
{isEdit
|
||||
? "Change issuer, domains, entry point or certificate. Provider ID cannot be changed."
|
||||
: "Add a SAML 2.0 identity provider (e.g. Okta SAML, Azure AD SAML, OneLogin). You need the IdP's SSO URL and signing certificate."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
@@ -167,8 +240,26 @@ export function RegisterSamlDialog({ children }: RegisterSamlDialogProps) {
|
||||
<Input
|
||||
placeholder="e.g. okta-saml or azure-saml"
|
||||
{...field}
|
||||
readOnly={isEdit}
|
||||
className={isEdit ? "bg-muted" : undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
{isEdit && (
|
||||
<FormDescription>
|
||||
Cannot be changed when editing.
|
||||
</FormDescription>
|
||||
)}
|
||||
{baseURL && (
|
||||
<div className="rounded-md bg-muted px-3 py-2 text-xs">
|
||||
<p className="font-medium text-muted-foreground">
|
||||
Callback URL (configure in your IdP)
|
||||
</p>
|
||||
<p className="mt-0.5 break-all font-mono">
|
||||
{baseURL}/api/auth/sso/saml2/callback/
|
||||
{watchedProviderId?.trim() || "..."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -317,7 +408,7 @@ export function RegisterSamlDialog({ children }: RegisterSamlDialogProps) {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" isLoading={isLoading}>
|
||||
Register provider
|
||||
{isEdit ? "Update provider" : "Register provider"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Shield,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { RegisterOidcDialog } from "./register-oidc-dialog";
|
||||
import { RegisterSamlDialog } from "./register-saml-dialog";
|
||||
|
||||
@@ -76,18 +77,12 @@ export const SSOSettings = () => {
|
||||
const utils = api.useUtils();
|
||||
const [detailsProvider, setDetailsProvider] =
|
||||
useState<ProviderForDetails | null>(null);
|
||||
const [baseURL, setBaseURL] = useState("");
|
||||
const baseURL = useUrl();
|
||||
const [manageOriginsOpen, setManageOriginsOpen] = useState(false);
|
||||
const [editingOrigin, setEditingOrigin] = useState<string | null>(null);
|
||||
const [editingValue, setEditingValue] = useState("");
|
||||
const [newOriginInput, setNewOriginInput] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
setBaseURL(window.location.origin);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { data: providers, isLoading } = api.sso.listProviders.useQuery();
|
||||
const { data: userData } = api.user.get.useQuery(undefined, {
|
||||
enabled: manageOriginsOpen,
|
||||
@@ -271,6 +266,22 @@ export const SSOSettings = () => {
|
||||
<Eye className="mr-1 size-3" />
|
||||
View details
|
||||
</Button>
|
||||
{isOidc && (
|
||||
<RegisterOidcDialog providerId={provider.providerId}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Pencil className="mr-1 size-3" />
|
||||
Edit
|
||||
</Button>
|
||||
</RegisterOidcDialog>
|
||||
)}
|
||||
{isSaml && (
|
||||
<RegisterSamlDialog providerId={provider.providerId}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Pencil className="mr-1 size-3" />
|
||||
Edit
|
||||
</Button>
|
||||
</RegisterSamlDialog>
|
||||
)}
|
||||
<DialogAction
|
||||
title="Remove SSO provider"
|
||||
description={`Remove provider "${provider.providerId}"? Users will no longer be able to sign in with this IdP.`}
|
||||
@@ -350,8 +361,7 @@ export const SSOSettings = () => {
|
||||
<DialogHeader>
|
||||
<DialogTitle>SSO provider details</DialogTitle>
|
||||
<DialogDescription>
|
||||
View-only. To change settings, remove this provider and add it
|
||||
again with the new values.
|
||||
Use Edit to change provider settings (OIDC or SAML).
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-3 py-2">
|
||||
|
||||
1
apps/dokploy/drizzle/0143_brown_ultron.sql
Normal file
1
apps/dokploy/drizzle/0143_brown_ultron.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "sso_provider" ADD COLUMN "created_at" timestamp DEFAULT now() NOT NULL;
|
||||
7291
apps/dokploy/drizzle/meta/0143_snapshot.json
Normal file
7291
apps/dokploy/drizzle/meta/0143_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1002,6 +1002,13 @@
|
||||
"when": 1770615019498,
|
||||
"tag": "0142_outstanding_tusk",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 143,
|
||||
"version": "7",
|
||||
"when": 1770961667210,
|
||||
"tag": "0143_brown_ultron",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -55,9 +55,128 @@ export const ssoRouter = createTRPCRouter({
|
||||
samlConfig: true,
|
||||
organizationId: true,
|
||||
},
|
||||
orderBy: [asc(ssoProvider.createdAt)],
|
||||
});
|
||||
return providers;
|
||||
}),
|
||||
one: enterpriseProcedure
|
||||
.input(z.object({ providerId: z.string().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const provider = await db.query.ssoProvider.findFirst({
|
||||
where: and(
|
||||
eq(ssoProvider.providerId, input.providerId),
|
||||
eq(ssoProvider.organizationId, ctx.session.activeOrganizationId),
|
||||
eq(ssoProvider.userId, ctx.session.userId),
|
||||
),
|
||||
columns: {
|
||||
id: true,
|
||||
providerId: true,
|
||||
issuer: true,
|
||||
domain: true,
|
||||
oidcConfig: true,
|
||||
samlConfig: true,
|
||||
organizationId: true,
|
||||
},
|
||||
});
|
||||
if (!provider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message:
|
||||
"SSO provider not found or you do not have permission to access it",
|
||||
});
|
||||
}
|
||||
return provider;
|
||||
}),
|
||||
update: enterpriseProcedure
|
||||
.input(ssoProviderBodySchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const existing = await db.query.ssoProvider.findFirst({
|
||||
where: and(
|
||||
eq(ssoProvider.providerId, input.providerId),
|
||||
eq(ssoProvider.organizationId, ctx.session.activeOrganizationId),
|
||||
eq(ssoProvider.userId, ctx.session.userId),
|
||||
),
|
||||
columns: {
|
||||
id: true,
|
||||
issuer: true,
|
||||
domain: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message:
|
||||
"SSO provider not found or you do not have permission to update it",
|
||||
});
|
||||
}
|
||||
|
||||
const providers = await db.query.ssoProvider.findMany({
|
||||
where: eq(ssoProvider.organizationId, ctx.session.activeOrganizationId),
|
||||
columns: { providerId: true, domain: true },
|
||||
});
|
||||
|
||||
for (const provider of providers) {
|
||||
if (provider.providerId === input.providerId) continue;
|
||||
const providerDomains = provider.domain
|
||||
.split(",")
|
||||
.map((d) => d.trim().toLowerCase());
|
||||
for (const domain of input.domains) {
|
||||
if (providerDomains.includes(domain)) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Domain ${domain} is already registered for another provider`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const issuerChanged =
|
||||
normalizeTrustedOrigin(existing.issuer) !==
|
||||
normalizeTrustedOrigin(input.issuer);
|
||||
if (issuerChanged) {
|
||||
const currentUser = await db.query.user.findFirst({
|
||||
where: eq(user.id, ctx.session.userId),
|
||||
columns: { trustedOrigins: true },
|
||||
});
|
||||
const trustedOrigins = currentUser?.trustedOrigins ?? [];
|
||||
const newOrigin = normalizeTrustedOrigin(input.issuer);
|
||||
const isInTrustedOrigins = trustedOrigins.some(
|
||||
(o) => o.toLowerCase() === newOrigin.toLowerCase(),
|
||||
);
|
||||
if (!isInTrustedOrigins) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"The new Issuer URL is not in your trusted origins list. Please add it in Manage origins before saving.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const domain = input.domains.join(",");
|
||||
const updateBody: {
|
||||
issuer: string;
|
||||
domain: string;
|
||||
oidcConfig?: (typeof input)["oidcConfig"];
|
||||
samlConfig?: (typeof input)["samlConfig"];
|
||||
} = {
|
||||
issuer: input.issuer,
|
||||
domain,
|
||||
};
|
||||
if (input.oidcConfig != null) {
|
||||
updateBody.oidcConfig = input.oidcConfig;
|
||||
}
|
||||
if (input.samlConfig != null) {
|
||||
updateBody.samlConfig = input.samlConfig;
|
||||
}
|
||||
|
||||
await auth.updateSSOProvider({
|
||||
params: { providerId: input.providerId },
|
||||
body: updateBody,
|
||||
headers: requestToHeaders(ctx.req),
|
||||
});
|
||||
return { success: true };
|
||||
}),
|
||||
deleteProvider: enterpriseProcedure
|
||||
.input(z.object({ providerId: z.string().min(1) }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@@ -102,24 +221,6 @@ export const ssoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const currentUser = await db.query.user.findFirst({
|
||||
where: eq(user.id, ctx.session.userId),
|
||||
columns: {
|
||||
trustedOrigins: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (currentUser?.trustedOrigins) {
|
||||
const issuerOrigin = normalizeTrustedOrigin(providerToDelete.issuer);
|
||||
const updatedOrigins = currentUser.trustedOrigins.filter(
|
||||
(origin) => origin.toLowerCase() !== issuerOrigin.toLowerCase(),
|
||||
);
|
||||
|
||||
await db
|
||||
.update(user)
|
||||
.set({ trustedOrigins: updatedOrigins })
|
||||
.where(eq(user.id, ctx.session.userId));
|
||||
}
|
||||
return { success: true };
|
||||
}),
|
||||
register: enterpriseProcedure
|
||||
@@ -147,25 +248,6 @@ export const ssoRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
const domain = input.domains.join(",");
|
||||
const currentUser = await db.query.user.findFirst({
|
||||
where: eq(user.id, ctx.session.userId),
|
||||
columns: {
|
||||
trustedOrigins: true,
|
||||
},
|
||||
});
|
||||
|
||||
const existingOrigins = currentUser?.trustedOrigins || [];
|
||||
|
||||
const issuerOrigin = normalizeTrustedOrigin(input.issuer);
|
||||
|
||||
const newOrigins = Array.from(
|
||||
new Set([...existingOrigins, issuerOrigin]),
|
||||
);
|
||||
|
||||
await db
|
||||
.update(user)
|
||||
.set({ trustedOrigins: newOrigins })
|
||||
.where(eq(user.id, ctx.session.userId));
|
||||
|
||||
await auth.registerSSOProvider({
|
||||
body: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
import { user } from "./user";
|
||||
@@ -15,6 +15,7 @@ export const ssoProvider = pgTable("sso_provider", {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
domain: text("domain").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({
|
||||
|
||||
@@ -347,6 +347,7 @@ export const auth = {
|
||||
handler,
|
||||
createApiKey: api.createApiKey,
|
||||
registerSSOProvider: api.registerSSOProvider,
|
||||
updateSSOProvider: api.updateSSOProvider,
|
||||
};
|
||||
|
||||
export const validateRequest = async (request: IncomingMessage) => {
|
||||
|
||||
Reference in New Issue
Block a user