({
values: {
@@ -145,7 +147,7 @@ export const SignUpForm = ({
const onSignUpWithGoogleClick = async () => {
try {
await authClient.google.signIn();
- } catch (err) {
+ } catch {
toast({
title: _(msg`An unknown error occurred`),
description: _(msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`),
@@ -157,7 +159,7 @@ export const SignUpForm = ({
const onSignUpWithMicrosoftClick = async () => {
try {
await authClient.microsoft.signIn();
- } catch (err) {
+ } catch {
toast({
title: _(msg`An unknown error occurred`),
description: _(msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`),
@@ -169,7 +171,7 @@ export const SignUpForm = ({
const onSignUpWithOIDCClick = async () => {
try {
await authClient.oidc.signIn();
- } catch (err) {
+ } catch {
toast({
title: _(msg`An unknown error occurred`),
description: _(msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`),
@@ -235,72 +237,80 @@ export const SignUpForm = ({
diff --git a/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx b/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
index cc546ec89..ed57ba9b7 100644
--- a/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
+++ b/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
@@ -1,6 +1,7 @@
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
+import { isSignupEnabledForProvider } from '@documenso/lib/constants/auth';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
@@ -8,7 +9,6 @@ import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-re
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
-import { env } from '@documenso/lib/utils/env';
import { trpc } from '@documenso/trpc/react';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import { SigningCard3D } from '@documenso/ui/components/signing-card';
@@ -77,7 +77,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
const recipientName =
recipient.name || fields.find((field) => field.type === FieldType.NAME)?.customText || recipient.email;
- const canSignUp = !isExistingUser && env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true';
+ const canSignUp = !isExistingUser && isSignupEnabledForProvider('email');
const canRedirectToFolder = user && document.userId === user.id && document.folderId && document.team?.url;
diff --git a/apps/remix/app/routes/_unauthenticated+/signin.tsx b/apps/remix/app/routes/_unauthenticated+/signin.tsx
index e497540a8..5ea5a5398 100644
--- a/apps/remix/app/routes/_unauthenticated+/signin.tsx
+++ b/apps/remix/app/routes/_unauthenticated+/signin.tsx
@@ -3,9 +3,9 @@ import {
IS_GOOGLE_SSO_ENABLED,
IS_MICROSOFT_SSO_ENABLED,
IS_OIDC_SSO_ENABLED,
+ isSignupEnabledForProvider,
OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth';
-import { env } from '@documenso/lib/utils/env';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { msg } from '@lingui/core/macro';
@@ -32,6 +32,11 @@ export async function loader({ request }: Route.LoaderArgs) {
const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED;
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
const oidcProviderLabel = OIDC_PROVIDER_LABEL;
+ const isSignupEnabled =
+ isSignupEnabledForProvider('email') ||
+ (IS_GOOGLE_SSO_ENABLED && isSignupEnabledForProvider('google')) ||
+ (IS_MICROSOFT_SSO_ENABLED && isSignupEnabledForProvider('microsoft')) ||
+ (IS_OIDC_SSO_ENABLED && isSignupEnabledForProvider('oidc'));
let returnTo = new URL(request.url).searchParams.get('returnTo') ?? undefined;
@@ -45,13 +50,15 @@ export async function loader({ request }: Route.LoaderArgs) {
isGoogleSSOEnabled,
isMicrosoftSSOEnabled,
isOIDCSSOEnabled,
+ isSignupEnabled,
oidcProviderLabel,
returnTo,
};
}
export default function SignIn({ loaderData }: Route.ComponentProps) {
- const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel, returnTo } = loaderData;
+ const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, isSignupEnabled, oidcProviderLabel, returnTo } =
+ loaderData;
const { _ } = useLingui();
@@ -95,7 +102,7 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
returnTo={returnTo}
/>
- {!isEmbeddedRedirect && env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true' && (
+ {!isEmbeddedRedirect && isSignupEnabled && (
Don't have an account?{' '}
diff --git a/apps/remix/app/routes/_unauthenticated+/signup.tsx b/apps/remix/app/routes/_unauthenticated+/signup.tsx
index 884aaeffb..f470004d2 100644
--- a/apps/remix/app/routes/_unauthenticated+/signup.tsx
+++ b/apps/remix/app/routes/_unauthenticated+/signup.tsx
@@ -1,5 +1,9 @@
-import { IS_GOOGLE_SSO_ENABLED, IS_MICROSOFT_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
-import { env } from '@documenso/lib/utils/env';
+import {
+ IS_GOOGLE_SSO_ENABLED,
+ IS_MICROSOFT_SSO_ENABLED,
+ IS_OIDC_SSO_ENABLED,
+ isSignupEnabledForProvider,
+} from '@documenso/lib/constants/auth';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { msg } from '@lingui/core/macro';
import { redirect } from 'react-router';
@@ -14,14 +18,15 @@ export function meta() {
}
export function loader({ request }: Route.LoaderArgs) {
- const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
+ const isEmailPasswordSignupEnabled = isSignupEnabledForProvider('email');
+ const isGoogleSignupEnabled = IS_GOOGLE_SSO_ENABLED && isSignupEnabledForProvider('google');
+ const isMicrosoftSignupEnabled = IS_MICROSOFT_SSO_ENABLED && isSignupEnabledForProvider('microsoft');
+ const isOidcSignupEnabled = IS_OIDC_SSO_ENABLED && isSignupEnabledForProvider('oidc');
- // SSR env variables.
- const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
- const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED;
- const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
+ const isAnySignupEnabled =
+ isEmailPasswordSignupEnabled || isGoogleSignupEnabled || isMicrosoftSignupEnabled || isOidcSignupEnabled;
- if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') {
+ if (!isAnySignupEnabled) {
throw redirect('/signin');
}
@@ -30,22 +35,30 @@ export function loader({ request }: Route.LoaderArgs) {
returnTo = isValidReturnTo(returnTo) ? normalizeReturnTo(returnTo) : undefined;
return {
- isGoogleSSOEnabled,
- isMicrosoftSSOEnabled,
- isOIDCSSOEnabled,
+ isEmailPasswordSignupEnabled,
+ isGoogleSignupEnabled,
+ isMicrosoftSignupEnabled,
+ isOidcSignupEnabled,
returnTo,
};
}
export default function SignUp({ loaderData }: Route.ComponentProps) {
- const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, returnTo } = loaderData;
+ const {
+ isEmailPasswordSignupEnabled,
+ isGoogleSignupEnabled,
+ isMicrosoftSignupEnabled,
+ isOidcSignupEnabled,
+ returnTo,
+ } = loaderData;
return (
);
diff --git a/docker/README.md b/docker/README.md
index e6564262f..f6c549760 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -253,5 +253,9 @@ Here's a markdown table documenting all the provided environment variables:
| `NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY` | The private key for DKIM signing with MailChannels for the `mailchannels` transport. |
| `NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT` | The maximum document upload limit displayed to the user (in MB). |
| `NEXT_PUBLIC_POSTHOG_KEY` | The optional PostHog key for analytics and feature flags. |
-| `NEXT_PUBLIC_DISABLE_SIGNUP` | Whether to disable user signups through the /signup page. |
+| `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch. Set to `true` to disable all signup methods (incl. organisation OIDC portal). |
+| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Set to `true` to disable email/password signup only. SSO signup is unaffected. |
+| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Set to `true` to block new accounts via Google. Existing Google-linked users can still sign in. |
+| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Set to `true` to block new accounts via Microsoft. Existing linked users can still sign in. |
+| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Set to `true` to block new accounts via OIDC (incl. organisation portal). Existing users unaffected.|
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`). |
diff --git a/docker/production/compose.yml b/docker/production/compose.yml
index 55e19294a..b84e9bf65 100644
--- a/docker/production/compose.yml
+++ b/docker/production/compose.yml
@@ -59,6 +59,10 @@ services:
- NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=${NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT}
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
- NEXT_PUBLIC_DISABLE_SIGNUP=${NEXT_PUBLIC_DISABLE_SIGNUP}
+ - NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=${NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP}
+ - NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP=${NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP}
+ - NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=${NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP}
+ - NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=${NEXT_PUBLIC_DISABLE_OIDC_SIGNUP}
- NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=${NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS}
- NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=${NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH:-/opt/documenso/cert.p12}
- NEXT_PRIVATE_SIGNING_PASSPHRASE=${NEXT_PRIVATE_SIGNING_PASSPHRASE}
diff --git a/packages/auth/server/lib/utils/handle-oauth-callback-url.ts b/packages/auth/server/lib/utils/handle-oauth-callback-url.ts
index abfa612f6..a8a9abe44 100644
--- a/packages/auth/server/lib/utils/handle-oauth-callback-url.ts
+++ b/packages/auth/server/lib/utils/handle-oauth-callback-url.ts
@@ -1,10 +1,12 @@
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { isEmailDomainAllowedForSignup } from '@documenso/lib/constants/auth';
+import {
+ isEmailDomainAllowedForSignup,
+ isSignupEnabledForProvider,
+} from '@documenso/lib/constants/auth';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
-import { env } from '@documenso/lib/utils/env';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@prisma/client';
@@ -115,8 +117,8 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
return c.redirect(redirectPath, 302);
}
- // Check if signups are disabled.
- if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
+ // Check if signups are disabled for this provider.
+ if (!isSignupEnabledForProvider(clientOptions.id as 'google' | 'microsoft' | 'oidc')) {
const errorUrl = new URL('/signin', NEXT_PUBLIC_WEBAPP_URL());
errorUrl.searchParams.set('error', AuthenticationErrorCode.SignupDisabled);
diff --git a/packages/auth/server/lib/utils/handle-oauth-organisation-callback-url.ts b/packages/auth/server/lib/utils/handle-oauth-organisation-callback-url.ts
index 92baedc5d..fbf99953f 100644
--- a/packages/auth/server/lib/utils/handle-oauth-organisation-callback-url.ts
+++ b/packages/auth/server/lib/utils/handle-oauth-organisation-callback-url.ts
@@ -1,4 +1,5 @@
import { sendOrganisationAccountLinkConfirmationEmail } from '@documenso/ee/server-only/lib/send-organisation-account-link-confirmation-email';
+import { isSignupEnabledForProvider } from '@documenso/lib/constants/auth';
import { AppError } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { formatOrganisationLoginUrl } from '@documenso/lib/utils/organisation-authentication-portal';
@@ -65,6 +66,14 @@ export const handleOAuthOrganisationCallbackUrl = async (options: HandleOAuthOrg
// Handle new user.
if (!userToLink) {
+ if (!isSignupEnabledForProvider('oidc')) {
+ const errorUrl = new URL(formatOrganisationLoginUrl(orgUrl));
+
+ errorUrl.searchParams.set('error', AuthenticationErrorCode.SignupDisabled);
+
+ return c.redirect(errorUrl.toString(), 302);
+ }
+
userToLink = await prisma.user.create({
data: {
email: email,
diff --git a/packages/auth/server/routes/email-password.ts b/packages/auth/server/routes/email-password.ts
index fa79cd76b..7ea0584ef 100644
--- a/packages/auth/server/routes/email-password.ts
+++ b/packages/auth/server/routes/email-password.ts
@@ -1,4 +1,4 @@
-import { isEmailDomainAllowedForSignup } from '@documenso/lib/constants/auth';
+import { isEmailDomainAllowedForSignup, isSignupEnabledForProvider } from '@documenso/lib/constants/auth';
import { EMAIL_VERIFICATION_STATE } from '@documenso/lib/constants/email';
import { AppError } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client';
@@ -27,7 +27,6 @@ import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/serv
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
-import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma';
import { sValidator } from '@hono/standard-validator';
import { compare } from '@node-rs/bcrypt';
@@ -184,7 +183,7 @@ export const emailPasswordRoute = new Hono()
.post('/signup', sValidator('json', ZSignUpSchema), async (c) => {
const requestMetadata = c.get('requestMetadata');
- if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
+ if (!isSignupEnabledForProvider('email')) {
throw new AppError(AuthenticationErrorCode.SignupDisabled, {
statusCode: 400,
});
diff --git a/packages/lib/constants/auth.ts b/packages/lib/constants/auth.ts
index 53614fa80..850c74e73 100644
--- a/packages/lib/constants/auth.ts
+++ b/packages/lib/constants/auth.ts
@@ -119,3 +119,24 @@ export const isEmailDomainAllowedForSignup = (email: string): boolean => {
return allowedDomains.includes(emailDomain);
};
+
+/**
+ * Check if signup is enabled for the given provider.
+ * The master switch takes precedence over the per-provider flags.
+ */
+export const isSignupEnabledForProvider = (
+ provider: 'email' | 'google' | 'microsoft' | 'oidc',
+): boolean => {
+ if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
+ return false;
+ }
+
+ const flagMap = {
+ email: 'NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP',
+ google: 'NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP',
+ microsoft: 'NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP',
+ oidc: 'NEXT_PUBLIC_DISABLE_OIDC_SIGNUP',
+ } as const;
+
+ return env(flagMap[provider]) !== 'true';
+};
diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts
index 16be71dc1..bb29fb271 100644
--- a/packages/tsconfig/process-env.d.ts
+++ b/packages/tsconfig/process-env.d.ts
@@ -74,6 +74,10 @@ declare namespace NodeJS {
NEXT_PRIVATE_SMTP_FROM_ADDRESS?: string;
NEXT_PUBLIC_DISABLE_SIGNUP?: string;
+ NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP?: string;
+ NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP?: string;
+ NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP?: string;
+ NEXT_PUBLIC_DISABLE_OIDC_SIGNUP?: string;
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS?: string;
NEXT_PRIVATE_BROWSERLESS_URL?: string;
diff --git a/render.yaml b/render.yaml
index 902672b0e..1811be308 100644
--- a/render.yaml
+++ b/render.yaml
@@ -155,6 +155,14 @@ services:
# Features Optional
- key: NEXT_PUBLIC_DISABLE_SIGNUP
sync: false
+ - key: NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP
+ sync: false
+ - key: NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP
+ sync: false
+ - key: NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP
+ sync: false
+ - key: NEXT_PUBLIC_DISABLE_OIDC_SIGNUP
+ sync: false
- key: NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS
sync: false
diff --git a/turbo.json b/turbo.json
index 2a0bbff9c..8fe691130 100644
--- a/turbo.json
+++ b/turbo.json
@@ -48,6 +48,10 @@
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
"NEXT_PUBLIC_DISABLE_SIGNUP",
+ "NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP",
+ "NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP",
+ "NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP",
+ "NEXT_PUBLIC_DISABLE_OIDC_SIGNUP",
"NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS",
"NEXT_PRIVATE_PLAIN_API_KEY",
"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT",