Implement SSO Sign-In Options: Add components for signing in with GitHub, Google, and SSO, enhancing user authentication methods. Update SSO settings to conditionally render based on enterprise features and improve the overall login experience on the homepage.

This commit is contained in:
Mauricio Siu
2026-01-30 22:28:17 -06:00
parent 66b4bf2c4e
commit f3d9960b7f
9 changed files with 326 additions and 213 deletions

View File

@@ -412,9 +412,9 @@ const MENU: Menu = {
title: "SSO",
url: "/dashboard/settings/sso",
icon: LogIn,
// Only enabled for admins in non-cloud environments (enterprise)
isEnabled: ({ auth, isCloud }) =>
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
// Enabled for admins in both cloud and self-hosted (enterprise)
isEnabled: ({ auth }) =>
!!(auth?.role === "owner" || auth?.role === "admin"),
},
],

View File

@@ -0,0 +1,47 @@
"use client";
import { useState } from "react";
import { toast } from "sonner";
import { authClient } from "@/lib/auth-client";
import { Button } from "@/components/ui/button";
export function SignInWithGithub() {
const [isLoading, setIsLoading] = useState(false);
const handleClick = async () => {
setIsLoading(true);
try {
const { error } = await authClient.signIn.social({
provider: "github",
});
if (error) {
toast.error(error.message);
return;
}
} catch (err) {
toast.error("An error occurred while signing in with GitHub", {
description: err instanceof Error ? err.message : "Unknown error",
});
} finally {
setIsLoading(false);
}
};
return (
<Button
variant="outline"
type="button"
className="w-full mb-4"
onClick={handleClick}
isLoading={isLoading}
>
<svg viewBox="0 0 438.549 438.549" className="mr-2 size-4">
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
/>
</svg>
Sign in with GitHub
</Button>
);
}

View File

@@ -0,0 +1,59 @@
"use client";
import { useState } from "react";
import { toast } from "sonner";
import { authClient } from "@/lib/auth-client";
import { Button } from "@/components/ui/button";
export function SignInWithGoogle() {
const [isLoading, setIsLoading] = useState(false);
const handleClick = async () => {
setIsLoading(true);
try {
const { error } = await authClient.signIn.social({
provider: "google",
});
if (error) {
toast.error(error.message);
return;
}
} catch (err) {
toast.error("An error occurred while signing in with Google", {
description: err instanceof Error ? err.message : "Unknown error",
});
} finally {
setIsLoading(false);
}
};
return (
<Button
variant="outline"
type="button"
className="w-full mb-4"
onClick={handleClick}
isLoading={isLoading}
>
<svg viewBox="0 0 24 24" className="mr-2 size-4">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</Button>
);
}

View File

@@ -0,0 +1,127 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2, LogIn } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
const ssoEmailSchema = z.object({
email: z
.string()
.min(1, "Enter your work email")
.email("Enter a valid email address")
.transform((v) => v.trim()),
});
type SSOEmailForm = z.infer<typeof ssoEmailSchema>;
interface SignInWithSSOProps {
/** Content shown when SSO is collapsed (e.g. email/password form) */
children: React.ReactNode;
}
export function SignInWithSSO({ children }: SignInWithSSOProps) {
const [expanded, setExpanded] = useState(false);
const form = useForm<SSOEmailForm>({
resolver: zodResolver(ssoEmailSchema),
defaultValues: { email: "" },
});
const onSubmit = async (values: SSOEmailForm) => {
try {
const { data, error } = await authClient.signIn.sso({
email: values.email,
callbackURL: "/dashboard/projects",
});
if (error) {
toast.error(error.message ?? "Failed to sign in with SSO");
return;
}
if (data?.url) {
window.location.href = data.url;
}
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to sign in with SSO",
);
}
};
if (!expanded) {
return (
<div className="mb-4 space-y-2">
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => setExpanded(true)}
>
<LogIn className="mr-2 size-4" />
Sign in with SSO
</Button>
{children}
</div>
);
}
return (
<div className="mb-4 space-y-2">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex gap-2">
<Input
type="email"
placeholder="you@company.com"
className="flex-1"
autoComplete="email"
disabled={form.formState.isSubmitting}
{...field}
/>
<Button
type="submit"
variant="outline"
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting ? (
<Loader2 className="size-4 animate-spin" />
) : (
"Continue"
)}
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<button
type="button"
onClick={() => setExpanded(false)}
className="text-xs text-muted-foreground hover:underline"
>
Use email and password instead
</button>
</form>
</Form>
</div>
);
}

View File

@@ -1,6 +1,6 @@
"use client";
import { Loader2, LogIn, Trash2, Eye } from "lucide-react";
import { Eye, Loader2, LogIn, Trash2 } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { DialogAction } from "@/components/shared/dialog-action";
@@ -63,10 +63,11 @@ function parseSamlConfig(
}
}
export function SSOSettings() {
export const SSOSettings = () => {
const utils = api.useUtils();
const [detailsProvider, setDetailsProvider] =
useState<ProviderForDetails | null>(null);
const { data: providers, isLoading } = api.sso.listProviders.useQuery();
const { mutateAsync: deleteProvider, isLoading: isDeleting } =
api.sso.deleteProvider.useMutation();
@@ -119,7 +120,10 @@ export function SSOSettings() {
const isSaml = !!provider.samlConfig;
return (
<Card key={provider.id} className="overflow-hidden">
<Card
key={provider.id}
className="overflow-hidden bg-background"
>
<CardHeader className="pb-2">
<div className="flex items-start justify-between gap-2">
<div className="flex flex-col gap-1">
@@ -352,4 +356,4 @@ export function SSOSettings() {
</Dialog>
</div>
);
}
};

View File

@@ -1,4 +1,4 @@
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
@@ -44,14 +44,6 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {

View File

@@ -3,7 +3,6 @@ import { validateRequest } from "@dokploy/server/lib/auth";
import { zodResolver } from "@hookform/resolvers/zod";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import type { GetServerSidePropsContext } from "next";
import { LogIn } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
@@ -11,6 +10,9 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { SignInWithGithub } from "@/components/proprietary/auth/sign-in-with-github";
import { SignInWithGoogle } from "@/components/proprietary/auth/sign-in-with-google";
import { SignInWithSSO } from "@/components/proprietary/sso/sign-in-with-sso";
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button } from "@/components/ui/button";
@@ -56,6 +58,7 @@ interface Props {
}
export default function Home({ IS_CLOUD }: Props) {
const router = useRouter();
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
const [isLoginLoading, setIsLoginLoading] = useState(false);
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false);
@@ -64,15 +67,6 @@ export default function Home({ IS_CLOUD }: Props) {
const [twoFactorCode, setTwoFactorCode] = useState("");
const [isBackupCodeModalOpen, setIsBackupCodeModalOpen] = useState(false);
const [backupCode, setBackupCode] = useState("");
const [isGithubLoading, setIsGithubLoading] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [isSSOLoading, setIsSSOLoading] = useState(false);
const { data: ssoProviders } = api.sso.listLoginProviders.useQuery(
undefined,
{
enabled: !IS_CLOUD,
},
);
const loginForm = useForm<LoginForm>({
resolver: zodResolver(LoginSchema),
defaultValues: {
@@ -170,69 +164,53 @@ export default function Home({ IS_CLOUD }: Props) {
}
};
const handleGithubSignIn = async () => {
setIsGithubLoading(true);
try {
const { error } = await authClient.signIn.social({
provider: "github",
});
if (error) {
toast.error(error.message);
return;
}
} catch (error) {
toast.error("An error occurred while signing in with GitHub", {
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsGithubLoading(false);
}
};
const handleGoogleSignIn = async () => {
setIsGoogleLoading(true);
try {
const { error } = await authClient.signIn.social({
provider: "google",
});
if (error) {
toast.error(error.message);
return;
}
} catch (error) {
toast.error("An error occurred while signing in with Google", {
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsGoogleLoading(false);
}
};
const handleSSOSignIn = async (providerId: string) => {
setIsSSOLoading(true);
try {
const { data, error } = await authClient.signIn.sso({
providerId,
callbackURL: "/dashboard/projects",
});
if (error) {
toast.error(error.message ?? "Failed to sign in with SSO");
return;
}
if (data?.url) {
window.location.href = data.url;
return;
}
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to sign in with SSO",
);
} finally {
setIsSSOLoading(false);
}
};
const loginContent = (
<>
{IS_CLOUD && <SignInWithGithub />}
{IS_CLOUD && <SignInWithGoogle />}
<Form {...loginForm}>
<form
onSubmit={loginForm.handleSubmit(onSubmit)}
className="space-y-4"
id="login-form"
>
<FormField
control={loginForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={loginForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button className="w-full" type="submit" isLoading={isLoginLoading}>
Login
</Button>
</form>
</Form>
</>
);
return (
<>
@@ -255,121 +233,11 @@ export default function Home({ IS_CLOUD }: Props) {
<CardContent className="p-0">
{!isTwoFactor ? (
<>
{IS_CLOUD && (
<Button
variant="outline"
type="button"
className="w-full mb-4"
onClick={handleGithubSignIn}
isLoading={isGithubLoading}
>
<svg viewBox="0 0 438.549 438.549" className="mr-2 size-4">
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
/>
</svg>
Sign in with GitHub
</Button>
{showSignInWithSSO ? (
<SignInWithSSO>{loginContent}</SignInWithSSO>
) : (
loginContent
)}
{IS_CLOUD && (
<Button
variant="outline"
type="button"
className="w-full mb-4"
onClick={handleGoogleSignIn}
isLoading={isGoogleLoading}
>
<svg viewBox="0 0 24 24" className="mr-2 size-4">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</Button>
)}
{!IS_CLOUD && ssoProviders && ssoProviders.length > 0 && (
<div className="mb-4 space-y-2">
<p className="text-center text-xs text-muted-foreground">
Sign in with SSO
</p>
<div className="flex flex-col gap-2">
{ssoProviders.map((provider) => (
<Button
key={provider.providerId}
variant="outline"
type="button"
className="w-full"
onClick={() => handleSSOSignIn(provider.providerId)}
disabled={isSSOLoading}
>
<LogIn className="mr-2 size-4" />
Sign in with{" "}
{provider.providerId.charAt(0).toUpperCase() +
provider.providerId.slice(1)}
</Button>
))}
</div>
</div>
)}
<Form {...loginForm}>
<form
onSubmit={loginForm.handleSubmit(onSubmit)}
className="space-y-4"
id="login-form"
>
<FormField
control={loginForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={loginForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
className="w-full"
type="submit"
isLoading={isLoginLoading}
>
Login
</Button>
</form>
</Form>
</>
) : (
<>

View File

@@ -168,7 +168,6 @@ export const licenseKeyRouter = createTRPCRouter({
currentUser?.isValidEnterpriseLicense
);
}),
updateEnterpriseSettings: adminProcedure
.input(
z.object({

View File

@@ -1,6 +1,7 @@
import { ssoProvider } from "@dokploy/server/db/schema";
import { IS_CLOUD } from "@dokploy/server/constants";
import { member, ssoProvider } from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm";
import { and, asc, eq } from "drizzle-orm";
import { z } from "zod";
import {
createTRPCRouter,
@@ -10,14 +11,31 @@ import {
import { db } from "@/server/db";
export const ssoRouter = createTRPCRouter({
/** Public list of SSO providers for the login page (providerId + issuer only). */
listLoginProviders: publicProcedure.query(async () => {
const providers = await db.query.ssoProvider.findMany({
columns: { providerId: true, issuer: true },
showSignInWithSSO: publicProcedure.query(async () => {
if (IS_CLOUD) {
return true;
}
const owner = await db.query.member.findFirst({
where: eq(member.role, "owner"),
with: {
user: {
columns: {
enableEnterpriseFeatures: true,
isValidEnterpriseLicense: true,
},
},
},
orderBy: [asc(member.createdAt)],
});
return providers;
}),
if (!owner) {
return false;
}
return (
owner.user.enableEnterpriseFeatures && owner.user.isValidEnterpriseLicense
);
}),
listProviders: enterpriseProcedure.query(async ({ ctx }) => {
const providers = await db.query.ssoProvider.findMany({
where: eq(ssoProvider.userId, ctx.user.id),
@@ -33,7 +51,6 @@ export const ssoRouter = createTRPCRouter({
});
return providers;
}),
deleteProvider: enterpriseProcedure
.input(z.object({ providerId: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {