diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 1dd025fc5..204bf9695 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -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"), }, ], diff --git a/apps/dokploy/components/proprietary/auth/sign-in-with-github.tsx b/apps/dokploy/components/proprietary/auth/sign-in-with-github.tsx new file mode 100644 index 000000000..988eeae05 --- /dev/null +++ b/apps/dokploy/components/proprietary/auth/sign-in-with-github.tsx @@ -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 ( + + ); +} diff --git a/apps/dokploy/components/proprietary/auth/sign-in-with-google.tsx b/apps/dokploy/components/proprietary/auth/sign-in-with-google.tsx new file mode 100644 index 000000000..bff0e69ab --- /dev/null +++ b/apps/dokploy/components/proprietary/auth/sign-in-with-google.tsx @@ -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 ( + + ); +} diff --git a/apps/dokploy/components/proprietary/sso/sign-in-with-sso.tsx b/apps/dokploy/components/proprietary/sso/sign-in-with-sso.tsx new file mode 100644 index 000000000..391329ed3 --- /dev/null +++ b/apps/dokploy/components/proprietary/sso/sign-in-with-sso.tsx @@ -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; + +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({ + 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 ( +
+ + {children} +
+ ); + } + + return ( +
+
+ + ( + + +
+ + +
+
+ +
+ )} + /> + + + +
+ ); +} diff --git a/apps/dokploy/components/proprietary/sso/sso-settings.tsx b/apps/dokploy/components/proprietary/sso/sso-settings.tsx index 850f65e88..8a4348c8f 100644 --- a/apps/dokploy/components/proprietary/sso/sso-settings.tsx +++ b/apps/dokploy/components/proprietary/sso/sso-settings.tsx @@ -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(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 ( - +
@@ -352,4 +356,4 @@ export function SSOSettings() {
); -} +}; diff --git a/apps/dokploy/pages/dashboard/settings/sso.tsx b/apps/dokploy/pages/dashboard/settings/sso.tsx index c0acedabb..164e2c3da 100644 --- a/apps/dokploy/pages/dashboard/settings/sso.tsx +++ b/apps/dokploy/pages/dashboard/settings/sso.tsx @@ -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 { diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx index c2d5df077..de4294581 100644 --- a/apps/dokploy/pages/index.tsx +++ b/apps/dokploy/pages/index.tsx @@ -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({ 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 && } + {IS_CLOUD && } +
+ + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + + + + + ); return ( <> @@ -255,121 +233,11 @@ export default function Home({ IS_CLOUD }: Props) { {!isTwoFactor ? ( <> - {IS_CLOUD && ( - + {showSignInWithSSO ? ( + {loginContent} + ) : ( + loginContent )} - {IS_CLOUD && ( - - )} - {!IS_CLOUD && ssoProviders && ssoProviders.length > 0 && ( -
-

- Sign in with SSO -

-
- {ssoProviders.map((provider) => ( - - ))} -
-
- )} -
- - ( - - Email - - - - - - )} - /> - ( - - Password - - - - - - )} - /> - - - ) : ( <> diff --git a/apps/dokploy/server/api/routers/proprietary/license-key.ts b/apps/dokploy/server/api/routers/proprietary/license-key.ts index d337b6cab..53816540c 100644 --- a/apps/dokploy/server/api/routers/proprietary/license-key.ts +++ b/apps/dokploy/server/api/routers/proprietary/license-key.ts @@ -168,7 +168,6 @@ export const licenseKeyRouter = createTRPCRouter({ currentUser?.isValidEnterpriseLicense ); }), - updateEnterpriseSettings: adminProcedure .input( z.object({ diff --git a/apps/dokploy/server/api/routers/proprietary/sso.ts b/apps/dokploy/server/api/routers/proprietary/sso.ts index dae7fe8d2..08483d96d 100644 --- a/apps/dokploy/server/api/routers/proprietary/sso.ts +++ b/apps/dokploy/server/api/routers/proprietary/sso.ts @@ -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 }) => {