feat: add granular signup disable flags (#2765)

This commit is contained in:
Ephraim Duncan
2026-05-09 01:16:13 +00:00
committed by GitHub
parent ec8728b33e
commit a197bf113f
18 changed files with 245 additions and 116 deletions
+9 -1
View File
@@ -160,8 +160,16 @@ NEXT_PRIVATE_REDIS_PREFIX="documenso"
NEXT_PUBLIC_POSTHOG_KEY="" NEXT_PUBLIC_POSTHOG_KEY=""
# OPTIONAL: Leave blank to disable billing. # OPTIONAL: Leave blank to disable billing.
NEXT_PUBLIC_FEATURE_BILLING_ENABLED= NEXT_PUBLIC_FEATURE_BILLING_ENABLED=
# OPTIONAL: Leave blank to allow users to signup through /signup page. # OPTIONAL: Set to "true" to disable all signup methods (email, Google, Microsoft, OIDC, including the organisation OIDC portal).
NEXT_PUBLIC_DISABLE_SIGNUP= NEXT_PUBLIC_DISABLE_SIGNUP=
# OPTIONAL: Set to "true" to disable email/password signup only.
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=
# OPTIONAL: Set to "true" to block new-account creation through Google. Existing linked users can still sign in.
NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP=
# OPTIONAL: Set to "true" to block new-account creation through Microsoft. Existing linked users can still sign in.
NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=
# OPTIONAL: Set to "true" to block new-account creation through OIDC (including the organisation portal).
NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=
# OPTIONAL: Comma-separated list of email domains allowed to sign up (e.g., example.com,acme.org). # OPTIONAL: Comma-separated list of email domains allowed to sign up (e.g., example.com,acme.org).
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS= NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=
# OPTIONAL: Set to true to use internal webapp url in browserless requests. # OPTIONAL: Set to true to use internal webapp url in browserless requests.
@@ -224,28 +224,41 @@ For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/con
## Feature Flags ## Feature Flags
| Variable | Description | Default | | Variable | Description | Default |
| ------------------------------------- | ----------------------------------------------------------------------------------- | ------- | | -------------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public user registration entirely | `false` | | `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch. Disable all signup methods application-wide | `false` |
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | | | `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only. SSO signup is unaffected | `false` |
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google. Existing Google-linked users can still sign in | `false` |
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft. Existing linked users can still sign in | `false` |
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC, including the organisation portal | `false` |
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | |
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | | | `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | |
| `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` | | `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` |
### Signup Restrictions ### Signup Restrictions
You can control who is allowed to create accounts on your instance using two environment variables: You can control who is allowed to create accounts on your instance with the following environment variables:
- **`NEXT_PUBLIC_DISABLE_SIGNUP`**: Set to `true` to block all new signups. Existing users can still sign in. This applies to both email/password and OAuth signups. - **`NEXT_PUBLIC_DISABLE_SIGNUP`** (master switch): Set to `true` to block all new signups across every method (email/password, Google, Microsoft, OIDC). When set, this also blocks new-account creation through the organisation OIDC authentication portal.
- **`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP`**: Set to `true` to disable email/password signup only. SSO signup is still allowed.
- **`NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP`**, **`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`**, **`NEXT_PUBLIC_DISABLE_OIDC_SIGNUP`**: Set to `true` to block brand-new account creation through the matching SSO provider. Existing users with the provider already linked can still sign in, and existing users can still link the provider to their account. `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` also blocks new-account creation through the organisation authentication portal.
- **`NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`**: Restrict signups to specific email domains. When set, only users whose email address matches one of the listed domains can create an account. Leave empty to allow all domains. - **`NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`**: Restrict signups to specific email domains. When set, only users whose email address matches one of the listed domains can create an account. Leave empty to allow all domains.
Both restrictions apply to email/password registration and OAuth (Google, Microsoft, OIDC). If a user attempts to sign up via OAuth with a disallowed domain, they are redirected to the sign-in page with an error. Sign-in for existing users is never affected — only the creation of brand-new accounts.
When both variables are set, `NEXT_PUBLIC_DISABLE_SIGNUP` takes precedence. Signups are blocked regardless of the domain list. Both the master switch and the domain allowlist apply to email/password registration and OAuth (Google, Microsoft, OIDC). If a user attempts to sign up via OAuth with a disallowed domain, they are redirected to the sign-in page with an error.
When both the master switch and the domain allowlist are set, the master switch takes precedence. Signups are blocked regardless of the domain list.
```bash ```bash
# Allow signups only from specific domains # Allow signups only from specific domains
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org" NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
# Allow OIDC signup only; block email/password, Google, Microsoft
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP="true"
NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP="true"
NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true"
# Or disable signups entirely # Or disable signups entirely
NEXT_PUBLIC_DISABLE_SIGNUP="true" NEXT_PUBLIC_DISABLE_SIGNUP="true"
``` ```
@@ -371,6 +384,10 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password"
# Signup restrictions (optional) # Signup restrictions (optional)
# NEXT_PUBLIC_DISABLE_SIGNUP="true" # NEXT_PUBLIC_DISABLE_SIGNUP="true"
# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP="true"
# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP="true"
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true"
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP="true"
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org" # NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
``` ```
@@ -155,7 +155,13 @@ PORT=3000
NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password
# Signup restrictions (optional) # Signup restrictions (optional)
# Master switch — disables every signup method
NEXT_PUBLIC_DISABLE_SIGNUP=false NEXT_PUBLIC_DISABLE_SIGNUP=false
# Per-method switches (optional). Each disables brand-new account creation through that method.
# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=true
# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP=true
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=true
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=true
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org # NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org
``` ```
@@ -252,7 +258,10 @@ Navigate to the signup page and create your account. Verify your email address
<Callout type="info"> <Callout type="info">
All accounts created through signup are regular user accounts. Admin access must be granted All accounts created through signup are regular user accounts. Admin access must be granted
directly in the database. Once your accounts are set up, consider disabling public signups by directly in the database. Once your accounts are set up, consider disabling public signups by
setting `NEXT_PUBLIC_DISABLE_SIGNUP=true`, or restrict signups to specific email domains with setting `NEXT_PUBLIC_DISABLE_SIGNUP=true`. For finer control, use the per-method switches
`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP`, `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP`,
`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`, `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP`, or restrict
signups to specific email domains with
`NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`. `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`.
</Callout> </Callout>
@@ -100,7 +100,11 @@ See [Email Configuration](/docs/self-hosting/configuration/email) for other tran
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for the signing certificate | - | | `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for the signing certificate | - |
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | Base64-encoded `.p12` certificate (alternative to file path) | - | | `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | Base64-encoded `.p12` certificate (alternative to file path) | - |
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Document storage: `database` or `s3` | `database` | | `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Document storage: `database` or `s3` | `database` |
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public signups | `false` | | `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch — disable all signup methods | `false` |
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only | `false` |
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google OAuth | `false` |
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft OAuth | `false` |
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal) | `false` |
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
For the complete list, see [Environment Variables](/docs/self-hosting/configuration/environment). For the complete list, see [Environment Variables](/docs/self-hosting/configuration/environment).
@@ -153,7 +153,11 @@ NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@yourdomain.com
| Variable | Description | Default | | Variable | Description | Default |
| --------------------------------- | ---------------------------------- | ------- | | --------------------------------- | ---------------------------------- | ------- |
| `PORT` | Application port | `3000` | | `PORT` | Application port | `3000` |
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public signups | `false` | | `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch — disable all signup methods | `false` |
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only | `false` |
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google OAuth | `false` |
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`| Block new accounts via Microsoft OAuth | `false` |
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal)| `false` |
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - | | `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - |
| `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` | | `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` |
+88 -76
View File
@@ -58,18 +58,20 @@ export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
export type SignUpFormProps = { export type SignUpFormProps = {
className?: string; className?: string;
initialEmail?: string; initialEmail?: string;
isGoogleSSOEnabled?: boolean; isEmailPasswordSignupEnabled?: boolean;
isMicrosoftSSOEnabled?: boolean; isGoogleSignupEnabled?: boolean;
isOIDCSSOEnabled?: boolean; isMicrosoftSignupEnabled?: boolean;
isOidcSignupEnabled?: boolean;
returnTo?: string; returnTo?: string;
}; };
export const SignUpForm = ({ export const SignUpForm = ({
className, className,
initialEmail, initialEmail,
isGoogleSSOEnabled, isEmailPasswordSignupEnabled = true,
isMicrosoftSSOEnabled, isGoogleSignupEnabled,
isOIDCSSOEnabled, isMicrosoftSignupEnabled,
isOidcSignupEnabled,
returnTo, returnTo,
}: SignUpFormProps) => { }: SignUpFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
@@ -86,7 +88,7 @@ export const SignUpForm = ({
const [captchaToken, setCaptchaToken] = useState<string | null>(null); const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const hasSocialAuthEnabled = isGoogleSSOEnabled || isMicrosoftSSOEnabled || isOIDCSSOEnabled; const hasSocialAuthEnabled = isGoogleSignupEnabled || isMicrosoftSignupEnabled || isOidcSignupEnabled;
const form = useForm<TSignUpFormSchema>({ const form = useForm<TSignUpFormSchema>({
values: { values: {
@@ -145,7 +147,7 @@ export const SignUpForm = ({
const onSignUpWithGoogleClick = async () => { const onSignUpWithGoogleClick = async () => {
try { try {
await authClient.google.signIn(); await authClient.google.signIn();
} catch (err) { } catch {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
description: _(msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`), 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 () => { const onSignUpWithMicrosoftClick = async () => {
try { try {
await authClient.microsoft.signIn(); await authClient.microsoft.signIn();
} catch (err) { } catch {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
description: _(msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`), 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 () => { const onSignUpWithOIDCClick = async () => {
try { try {
await authClient.oidc.signIn(); await authClient.oidc.signIn();
} catch (err) { } catch {
toast({ toast({
title: _(msg`An unknown error occurred`), title: _(msg`An unknown error occurred`),
description: _(msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`), description: _(msg`We encountered an unknown error while attempting to sign you Up. Please try again later.`),
@@ -235,72 +237,80 @@ export const SignUpForm = ({
<Form {...form}> <Form {...form}>
<form className="flex w-full flex-1 flex-col gap-y-4" onSubmit={form.handleSubmit(onFormSubmit)}> <form className="flex w-full flex-1 flex-col gap-y-4" onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSubmitting}> <fieldset className="flex w-full flex-col gap-y-4" disabled={isSubmitting}>
<FormField {isEmailPasswordSignupEnabled && (
control={form.control} <>
name="name" <FormField
render={({ field }) => ( control={form.control}
<FormItem> name="name"
<FormLabel> render={({ field }) => (
<Trans>Full Name</Trans> <FormItem>
</FormLabel> <FormLabel>
<FormControl> <Trans>Full Name</Trans>
<Input type="text" {...field} /> </FormLabel>
</FormControl> <FormControl>
<FormMessage /> <Input type="text" {...field} />
</FormItem> </FormControl>
)} <FormMessage />
/> </FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
<Trans>Email Address</Trans> <Trans>Email Address</Trans>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input type="email" {...field} /> <Input type="email" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="password" name="password"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
<Trans>Password</Trans> <Trans>Password</Trans>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<PasswordInput {...field} /> <PasswordInput {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="signature" name="signature"
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
<Trans>Sign Here</Trans> <Trans>Sign Here</Trans>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<SignaturePadDialog disabled={isSubmitting} value={value} onChange={(v) => onChange(v ?? '')} /> <SignaturePadDialog
</FormControl> disabled={isSubmitting}
value={value}
onChange={(v) => onChange(v ?? '')}
/>
</FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</>
)}
{turnstileSiteKey && ( {turnstileSiteKey && (
<Turnstile <Turnstile
@@ -325,7 +335,7 @@ export const SignUpForm = ({
</div> </div>
)} )}
{isGoogleSSOEnabled && ( {isGoogleSignupEnabled && (
<Button <Button
type="button" type="button"
size="lg" size="lg"
@@ -339,7 +349,7 @@ export const SignUpForm = ({
</Button> </Button>
)} )}
{isMicrosoftSSOEnabled && ( {isMicrosoftSignupEnabled && (
<Button <Button
type="button" type="button"
size="lg" size="lg"
@@ -353,7 +363,7 @@ export const SignUpForm = ({
</Button> </Button>
)} )}
{isOIDCSSOEnabled && ( {isOidcSignupEnabled && (
<Button <Button
type="button" type="button"
size="lg" size="lg"
@@ -377,9 +387,11 @@ export const SignUpForm = ({
</p> </p>
</fieldset> </fieldset>
<Button loading={form.formState.isSubmitting} type="submit" size="lg" className="mt-6 w-full"> {isEmailPasswordSignupEnabled && (
<Trans>Create account</Trans> <Button loading={form.formState.isSubmitting} type="submit" size="lg" className="mt-6 w-full">
</Button> <Trans>Create account</Trans>
</Button>
)}
</form> </form>
</Form> </Form>
<p className="mt-6 text-muted-foreground text-xs"> <p className="mt-6 text-muted-foreground text-xs">
@@ -1,6 +1,7 @@
import signingCelebration from '@documenso/assets/images/signing-celebration.png'; import signingCelebration from '@documenso/assets/images/signing-celebration.png';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session'; import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { useOptionalSession } from '@documenso/lib/client-only/providers/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 { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized'; import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; 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 { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email'; import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { env } from '@documenso/lib/utils/env';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import { SigningCard3D } from '@documenso/ui/components/signing-card'; import { SigningCard3D } from '@documenso/ui/components/signing-card';
@@ -77,7 +77,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
const recipientName = const recipientName =
recipient.name || fields.find((field) => field.type === FieldType.NAME)?.customText || recipient.email; 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; const canRedirectToFolder = user && document.userId === user.id && document.folderId && document.team?.url;
@@ -3,9 +3,9 @@ import {
IS_GOOGLE_SSO_ENABLED, IS_GOOGLE_SSO_ENABLED,
IS_MICROSOFT_SSO_ENABLED, IS_MICROSOFT_SSO_ENABLED,
IS_OIDC_SSO_ENABLED, IS_OIDC_SSO_ENABLED,
isSignupEnabledForProvider,
OIDC_PROVIDER_LABEL, OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth'; } from '@documenso/lib/constants/auth';
import { env } from '@documenso/lib/utils/env';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to'; import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
@@ -32,6 +32,11 @@ export async function loader({ request }: Route.LoaderArgs) {
const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED; const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED;
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED; const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
const oidcProviderLabel = OIDC_PROVIDER_LABEL; 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; let returnTo = new URL(request.url).searchParams.get('returnTo') ?? undefined;
@@ -45,13 +50,15 @@ export async function loader({ request }: Route.LoaderArgs) {
isGoogleSSOEnabled, isGoogleSSOEnabled,
isMicrosoftSSOEnabled, isMicrosoftSSOEnabled,
isOIDCSSOEnabled, isOIDCSSOEnabled,
isSignupEnabled,
oidcProviderLabel, oidcProviderLabel,
returnTo, returnTo,
}; };
} }
export default function SignIn({ loaderData }: Route.ComponentProps) { export default function SignIn({ loaderData }: Route.ComponentProps) {
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel, returnTo } = loaderData; const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, isSignupEnabled, oidcProviderLabel, returnTo } =
loaderData;
const { _ } = useLingui(); const { _ } = useLingui();
@@ -95,7 +102,7 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
returnTo={returnTo} returnTo={returnTo}
/> />
{!isEmbeddedRedirect && env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true' && ( {!isEmbeddedRedirect && isSignupEnabled && (
<p className="mt-6 text-center text-muted-foreground text-sm"> <p className="mt-6 text-center text-muted-foreground text-sm">
<Trans> <Trans>
Don't have an account?{' '} Don't have an account?{' '}
@@ -1,5 +1,9 @@
import { IS_GOOGLE_SSO_ENABLED, IS_MICROSOFT_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth'; import {
import { env } from '@documenso/lib/utils/env'; 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 { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { redirect } from 'react-router'; import { redirect } from 'react-router';
@@ -14,14 +18,15 @@ export function meta() {
} }
export function loader({ request }: Route.LoaderArgs) { 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 isAnySignupEnabled =
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED; isEmailPasswordSignupEnabled || isGoogleSignupEnabled || isMicrosoftSignupEnabled || isOidcSignupEnabled;
const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED;
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
if (NEXT_PUBLIC_DISABLE_SIGNUP === 'true') { if (!isAnySignupEnabled) {
throw redirect('/signin'); throw redirect('/signin');
} }
@@ -30,22 +35,30 @@ export function loader({ request }: Route.LoaderArgs) {
returnTo = isValidReturnTo(returnTo) ? normalizeReturnTo(returnTo) : undefined; returnTo = isValidReturnTo(returnTo) ? normalizeReturnTo(returnTo) : undefined;
return { return {
isGoogleSSOEnabled, isEmailPasswordSignupEnabled,
isMicrosoftSSOEnabled, isGoogleSignupEnabled,
isOIDCSSOEnabled, isMicrosoftSignupEnabled,
isOidcSignupEnabled,
returnTo, returnTo,
}; };
} }
export default function SignUp({ loaderData }: Route.ComponentProps) { export default function SignUp({ loaderData }: Route.ComponentProps) {
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, returnTo } = loaderData; const {
isEmailPasswordSignupEnabled,
isGoogleSignupEnabled,
isMicrosoftSignupEnabled,
isOidcSignupEnabled,
returnTo,
} = loaderData;
return ( return (
<SignUpForm <SignUpForm
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16" className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16"
isGoogleSSOEnabled={isGoogleSSOEnabled} isEmailPasswordSignupEnabled={isEmailPasswordSignupEnabled}
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled} isGoogleSignupEnabled={isGoogleSignupEnabled}
isOIDCSSOEnabled={isOIDCSSOEnabled} isMicrosoftSignupEnabled={isMicrosoftSignupEnabled}
isOidcSignupEnabled={isOidcSignupEnabled}
returnTo={returnTo} returnTo={returnTo}
/> />
); );
+5 -1
View File
@@ -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_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_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_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`). | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`). |
+4
View File
@@ -59,6 +59,10 @@ services:
- NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=${NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT} - NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=${NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT}
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY} - NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
- NEXT_PUBLIC_DISABLE_SIGNUP=${NEXT_PUBLIC_DISABLE_SIGNUP} - 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_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_LOCAL_FILE_PATH=${NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH:-/opt/documenso/cert.p12}
- NEXT_PRIVATE_SIGNING_PASSPHRASE=${NEXT_PRIVATE_SIGNING_PASSPHRASE} - NEXT_PRIVATE_SIGNING_PASSPHRASE=${NEXT_PRIVATE_SIGNING_PASSPHRASE}
@@ -1,10 +1,12 @@
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; 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 { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user'; import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account'; 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 { 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 { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@prisma/client'; import { UserSecurityAuditLogType } from '@prisma/client';
@@ -115,8 +117,8 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
return c.redirect(redirectPath, 302); return c.redirect(redirectPath, 302);
} }
// Check if signups are disabled. // Check if signups are disabled for this provider.
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') { if (!isSignupEnabledForProvider(clientOptions.id as 'google' | 'microsoft' | 'oidc')) {
const errorUrl = new URL('/signin', NEXT_PUBLIC_WEBAPP_URL()); const errorUrl = new URL('/signin', NEXT_PUBLIC_WEBAPP_URL());
errorUrl.searchParams.set('error', AuthenticationErrorCode.SignupDisabled); errorUrl.searchParams.set('error', AuthenticationErrorCode.SignupDisabled);
@@ -1,4 +1,5 @@
import { sendOrganisationAccountLinkConfirmationEmail } from '@documenso/ee/server-only/lib/send-organisation-account-link-confirmation-email'; 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 { AppError } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user'; import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { formatOrganisationLoginUrl } from '@documenso/lib/utils/organisation-authentication-portal'; import { formatOrganisationLoginUrl } from '@documenso/lib/utils/organisation-authentication-portal';
@@ -65,6 +66,14 @@ export const handleOAuthOrganisationCallbackUrl = async (options: HandleOAuthOrg
// Handle new user. // Handle new user.
if (!userToLink) { 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({ userToLink = await prisma.user.create({
data: { data: {
email: email, email: email,
@@ -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 { EMAIL_VERIFICATION_STATE } from '@documenso/lib/constants/email';
import { AppError } from '@documenso/lib/errors/app-error'; import { AppError } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client'; 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 { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import { updatePassword } from '@documenso/lib/server-only/user/update-password'; import { updatePassword } from '@documenso/lib/server-only/user/update-password';
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email'; import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
import { env } from '@documenso/lib/utils/env';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { sValidator } from '@hono/standard-validator'; import { sValidator } from '@hono/standard-validator';
import { compare } from '@node-rs/bcrypt'; import { compare } from '@node-rs/bcrypt';
@@ -184,7 +183,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
.post('/signup', sValidator('json', ZSignUpSchema), async (c) => { .post('/signup', sValidator('json', ZSignUpSchema), async (c) => {
const requestMetadata = c.get('requestMetadata'); const requestMetadata = c.get('requestMetadata');
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') { if (!isSignupEnabledForProvider('email')) {
throw new AppError(AuthenticationErrorCode.SignupDisabled, { throw new AppError(AuthenticationErrorCode.SignupDisabled, {
statusCode: 400, statusCode: 400,
}); });
+21
View File
@@ -119,3 +119,24 @@ export const isEmailDomainAllowedForSignup = (email: string): boolean => {
return allowedDomains.includes(emailDomain); 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';
};
+4
View File
@@ -74,6 +74,10 @@ declare namespace NodeJS {
NEXT_PRIVATE_SMTP_FROM_ADDRESS?: string; NEXT_PRIVATE_SMTP_FROM_ADDRESS?: string;
NEXT_PUBLIC_DISABLE_SIGNUP?: 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_ALLOWED_SIGNUP_DOMAINS?: string;
NEXT_PRIVATE_BROWSERLESS_URL?: string; NEXT_PRIVATE_BROWSERLESS_URL?: string;
+8
View File
@@ -155,6 +155,14 @@ services:
# Features Optional # Features Optional
- key: NEXT_PUBLIC_DISABLE_SIGNUP - key: NEXT_PUBLIC_DISABLE_SIGNUP
sync: false 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 - key: NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS
sync: false sync: false
+4
View File
@@ -48,6 +48,10 @@
"NEXT_PUBLIC_POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED", "NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
"NEXT_PUBLIC_DISABLE_SIGNUP", "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_ALLOWED_SIGNUP_DOMAINS",
"NEXT_PRIVATE_PLAIN_API_KEY", "NEXT_PRIVATE_PLAIN_API_KEY",
"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT", "NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT",