-
-
OR
-
+
+ {
+ setEmail(e.target.value.toLowerCase());
+ }}
+ />
}
+ disabled={loading || emailSent}
>
-
- Login with Google
+ {emailSent
+ ? NODE_ENV === "development"
+ ? "Email sent to your terminal"
+ : "Email sent to your inbox"
+ : "Continue with Email"}
>
)}
+ {(publicEnv.googleAuthAvailable ||
+ publicEnv.authentikAuthAvailable) && (
+ <>
+ {!publicEnv.disableEmailAuth && (
+
+ )}
+ {publicEnv.authentikAuthAvailable && (
+
+ )}
+ {publicEnv.googleAuthAvailable && (
+
+ )}
+ >
+ )}
);
};
diff --git a/apps/web/utils/public-env.tsx b/apps/web/utils/public-env.tsx
index 94999923909..2d4a8e83cb4 100644
--- a/apps/web/utils/public-env.tsx
+++ b/apps/web/utils/public-env.tsx
@@ -6,6 +6,8 @@ type PublicEnvContext = {
webUrl: string;
googleAuthAvailable: boolean;
workosAuthAvailable: boolean;
+ authentikAuthAvailable: boolean;
+ disableEmailAuth: boolean;
};
const Context = createContext
(null);
diff --git a/packages/database/auth/auth-options.ts b/packages/database/auth/auth-options.ts
index 1e3a886b2b1..70404c84182 100644
--- a/packages/database/auth/auth-options.ts
+++ b/packages/database/auth/auth-options.ts
@@ -6,6 +6,7 @@ import type { NextAuthOptions } from "next-auth";
import { getServerSession as _getServerSession } from "next-auth";
import type { Adapter } from "next-auth/adapters";
import { decode, type JWT, type JWTDecodeParams } from "next-auth/jwt";
+import AuthentikProvider from "next-auth/providers/authentik";
import EmailProvider from "next-auth/providers/email";
import GoogleProvider from "next-auth/providers/google";
import type { Provider } from "next-auth/providers/index";
@@ -68,7 +69,30 @@ export const authOptions = (): NextAuthOptions => {
},
get providers() {
if (_providers) return _providers;
+ const authentikIssuer = serverEnv().AUTHENTIK_ISSUER;
+ const authentikClientId = serverEnv().AUTHENTIK_CLIENT_ID;
+ const authentikClientSecret = serverEnv().AUTHENTIK_CLIENT_SECRET;
+ if (
+ authentikIssuer &&
+ (!authentikClientId || !authentikClientSecret)
+ ) {
+ throw new Error(
+ "AUTHENTIK_ISSUER is set but AUTHENTIK_CLIENT_ID and/or " +
+ "AUTHENTIK_CLIENT_SECRET is missing. All three must be " +
+ "provided together to enable the Authentik OIDC provider.",
+ );
+ }
+
_providers = [
+ ...(authentikIssuer && authentikClientId && authentikClientSecret
+ ? [
+ AuthentikProvider({
+ clientId: authentikClientId,
+ clientSecret: authentikClientSecret,
+ issuer: authentikIssuer,
+ }),
+ ]
+ : []),
GoogleProvider({
clientId: serverEnv().GOOGLE_CLIENT_ID as string,
clientSecret: serverEnv().GOOGLE_CLIENT_SECRET as string,
diff --git a/packages/env/server.ts b/packages/env/server.ts
index febc9e3e873..a36f9939885 100644
--- a/packages/env/server.ts
+++ b/packages/env/server.ts
@@ -72,6 +72,14 @@ function createServerEnv() {
WORKOS_CLIENT_ID: z.string().optional(),
WORKOS_API_KEY: z.string().optional(),
+ /// Authentik OIDC (self-hosted SSO)
+ // Provide these to enable a "Sign in with Authentik" provider.
+ // AUTHENTIK_ISSUER must include the application slug; no trailing slash:
+ // https://auth.example.com/application/o/
+ AUTHENTIK_CLIENT_ID: z.string().optional(),
+ AUTHENTIK_CLIENT_SECRET: z.string().optional(),
+ AUTHENTIK_ISSUER: z.string().optional(),
+
/// Settings
CAP_VIDEOS_DEFAULT_PUBLIC: boolString(true).describe(
"Should videos be public or private by default",
@@ -80,6 +88,10 @@ function createServerEnv() {
.string()
.optional()
.describe("Comma-separated list of permitted signup domains"),
+ CAP_DISABLE_EMAIL_AUTH: boolString(false).describe(
+ "Hide the email magic-link UI on login/signup/share pages. "
+ + "The provider stays wired server-side as break-glass.",
+ ),
/// AI providers
DEEPGRAM_API_KEY: z.string().optional().describe("Audio transcription"),