mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
feat: add email domain restriction for signups (#2266)
Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
This commit is contained in:
@@ -153,6 +153,8 @@ NEXT_PUBLIC_POSTHOG_KEY=""
|
||||
NEXT_PUBLIC_FEATURE_BILLING_ENABLED=
|
||||
# OPTIONAL: Leave blank to allow users to signup through /signup page.
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP=
|
||||
# OPTIONAL: Comma-separated list of email domains allowed to sign up (e.g., example.com,acme.org).
|
||||
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=
|
||||
# OPTIONAL: Set to true to use internal webapp url in browserless requests.
|
||||
NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false
|
||||
|
||||
|
||||
@@ -224,11 +224,31 @@ For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/con
|
||||
|
||||
## Feature Flags
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------------- | ----------------------------------------------- | ------- |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public user registration | `false` |
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | |
|
||||
| `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` |
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public user registration entirely | `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_FEATURE_BILLING_ENABLED` | Enable billing features | `false` |
|
||||
|
||||
### Signup Restrictions
|
||||
|
||||
You can control who is allowed to create accounts on your instance using two 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_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.
|
||||
|
||||
When both variables are set, `NEXT_PUBLIC_DISABLE_SIGNUP` takes precedence. Signups are blocked regardless of the domain list.
|
||||
|
||||
```bash
|
||||
# Allow signups only from specific domains
|
||||
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
|
||||
|
||||
# Or disable signups entirely
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP="true"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -328,6 +348,10 @@ NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@example.com"
|
||||
|
||||
# Signing (certificate must be configured)
|
||||
NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password"
|
||||
|
||||
# Signup restrictions (optional)
|
||||
# NEXT_PUBLIC_DISABLE_SIGNUP="true"
|
||||
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -154,8 +154,9 @@ PORT=3000
|
||||
# Signing certificate (see Signing Certificate section)
|
||||
NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password
|
||||
|
||||
# Disable public signups
|
||||
# Signup restrictions (optional)
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP=false
|
||||
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org
|
||||
```
|
||||
|
||||
<Callout type="info">Generate secure secrets using: `openssl rand -base64 32`</Callout>
|
||||
@@ -251,7 +252,8 @@ Navigate to the signup page and create your account. Verify your email address
|
||||
<Callout type="info">
|
||||
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
|
||||
setting `NEXT_PUBLIC_DISABLE_SIGNUP=true`.
|
||||
setting `NEXT_PUBLIC_DISABLE_SIGNUP=true`, or restrict signups to specific email domains with
|
||||
`NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`.
|
||||
</Callout>
|
||||
|
||||
## Managing Services
|
||||
|
||||
@@ -101,6 +101,7 @@ See [Email Configuration](/docs/self-hosting/configuration/email) for other tran
|
||||
| `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_DISABLE_SIGNUP` | Disable public signups | `false` |
|
||||
| `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).
|
||||
|
||||
|
||||
@@ -153,8 +153,9 @@ NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@yourdomain.com
|
||||
| Variable | Description | Default |
|
||||
| --------------------------------- | ---------------------------------- | ------- |
|
||||
| `PORT` | Application port | `3000` |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public signups | `false` |
|
||||
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public signups | `false` |
|
||||
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
|
||||
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - |
|
||||
| `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` |
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -54,8 +54,8 @@ export const ZSignUpFormSchema = z
|
||||
},
|
||||
);
|
||||
|
||||
export const signupErrorMessages: Record<string, MessageDescriptor> = {
|
||||
SIGNUP_DISABLED: msg`Signups are disabled.`,
|
||||
export const SIGNUP_ERROR_MESSAGES: Record<string, MessageDescriptor> = {
|
||||
SIGNUP_DISABLED: msg`Signup is currently disabled or not available for your email domain.`,
|
||||
[AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`,
|
||||
[AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`,
|
||||
};
|
||||
@@ -130,7 +130,8 @@ export const SignUpForm = ({
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
|
||||
const errorMessage =
|
||||
SIGNUP_ERROR_MESSAGES[error.code] ?? SIGNUP_ERROR_MESSAGES.INVALID_REQUEST;
|
||||
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
@@ -196,7 +197,7 @@ export const SignUpForm = ({
|
||||
|
||||
return (
|
||||
<div className={cn('flex justify-center gap-x-12', className)}>
|
||||
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
|
||||
<div className="relative hidden flex-1 overflow-hidden rounded-xl border border-border xl:flex">
|
||||
<div className="absolute -inset-8 -z-[2] backdrop-blur">
|
||||
<img
|
||||
src={communityCardsImage}
|
||||
@@ -205,17 +206,17 @@ export const SignUpForm = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-background/50 absolute -inset-8 -z-[1] backdrop-blur-[2px]" />
|
||||
<div className="absolute -inset-8 -z-[1] bg-background/50 backdrop-blur-[2px]" />
|
||||
|
||||
<div className="relative flex h-full w-full flex-col items-center justify-evenly">
|
||||
<div className="bg-background rounded-2xl border px-4 py-1 text-sm font-medium">
|
||||
<div className="rounded-2xl border bg-background px-4 py-1 text-sm font-medium">
|
||||
<Trans>User profiles are here!</Trans>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-md">
|
||||
<UserProfileTimur
|
||||
rows={2}
|
||||
className="bg-background border-border rounded-2xl border shadow-md"
|
||||
className="rounded-2xl border border-border bg-background shadow-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -223,13 +224,13 @@ export const SignUpForm = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-border dark:bg-background relative z-10 flex min-h-[min(850px,80vh)] w-full max-w-lg flex-col rounded-xl border bg-neutral-100 p-6">
|
||||
<div className="relative z-10 flex min-h-[min(850px,80vh)] w-full max-w-lg flex-col rounded-xl border border-border bg-neutral-100 p-6 dark:bg-background">
|
||||
<div className="h-20">
|
||||
<h1 className="text-xl font-semibold md:text-2xl">
|
||||
<Trans>Create a new account</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs md:text-sm">
|
||||
<p className="mt-2 text-xs text-muted-foreground md:text-sm">
|
||||
<Trans>
|
||||
Create your account and start using state-of-the-art document signing. Open and
|
||||
beautiful signing is within your grasp.
|
||||
@@ -323,70 +324,62 @@ export const SignUpForm = ({
|
||||
/>
|
||||
|
||||
{hasSocialAuthEnabled && (
|
||||
<>
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">
|
||||
<Trans>Or</Trans>
|
||||
</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
</>
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className="bg-transparent text-muted-foreground">
|
||||
<Trans>Or</Trans>
|
||||
</span>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isGoogleSSOEnabled && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant={'outline'}
|
||||
className="bg-background text-muted-foreground border"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignUpWithGoogleClick}
|
||||
>
|
||||
<FcGoogle className="mr-2 h-5 w-5" />
|
||||
<Trans>Sign Up with Google</Trans>
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant={'outline'}
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignUpWithGoogleClick}
|
||||
>
|
||||
<FcGoogle className="mr-2 h-5 w-5" />
|
||||
<Trans>Sign Up with Google</Trans>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isMicrosoftSSOEnabled && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant={'outline'}
|
||||
className="bg-background text-muted-foreground border"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignUpWithMicrosoftClick}
|
||||
>
|
||||
<img
|
||||
className="mr-2 h-4 w-4"
|
||||
alt="Microsoft Logo"
|
||||
src={'/static/microsoft.svg'}
|
||||
/>
|
||||
<Trans>Sign Up with Microsoft</Trans>
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant={'outline'}
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignUpWithMicrosoftClick}
|
||||
>
|
||||
<img
|
||||
className="mr-2 h-4 w-4"
|
||||
alt="Microsoft Logo"
|
||||
src={'/static/microsoft.svg'}
|
||||
/>
|
||||
<Trans>Sign Up with Microsoft</Trans>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isOIDCSSOEnabled && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant={'outline'}
|
||||
className="bg-background text-muted-foreground border"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignUpWithOIDCClick}
|
||||
>
|
||||
<FaIdCardClip className="mr-2 h-5 w-5" />
|
||||
<Trans>Sign Up with OIDC</Trans>
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant={'outline'}
|
||||
className="border bg-background text-muted-foreground"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignUpWithOIDCClick}
|
||||
>
|
||||
<FaIdCardClip className="mr-2 h-5 w-5" />
|
||||
<Trans>Sign Up with OIDC</Trans>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-sm">
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
<Trans>
|
||||
Already have an account?{' '}
|
||||
<Link to="/signin" className="text-documenso-700 duration-200 hover:opacity-70">
|
||||
@@ -406,7 +399,7 @@ export const SignUpForm = ({
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
<p className="text-muted-foreground mt-6 text-xs">
|
||||
<p className="mt-6 text-xs text-muted-foreground">
|
||||
<Trans>
|
||||
By proceeding, you agree to our{' '}
|
||||
<Link
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Input } from '@documenso/ui/primitives/input';
|
||||
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { signupErrorMessages } from '~/components/forms/signup';
|
||||
import { SIGNUP_ERROR_MESSAGES } from '~/components/forms/signup';
|
||||
|
||||
export type ClaimAccountProps = {
|
||||
defaultName: string;
|
||||
@@ -90,7 +90,8 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = signupErrorMessages[error.code] ?? signupErrorMessages.INVALID_REQUEST;
|
||||
const errorMessage =
|
||||
SIGNUP_ERROR_MESSAGES[error.code] ?? SIGNUP_ERROR_MESSAGES.INVALID_REQUEST;
|
||||
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Link, redirect } from 'react-router';
|
||||
import { Link, redirect, useSearchParams } from 'react-router';
|
||||
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import {
|
||||
@@ -12,8 +13,10 @@ import {
|
||||
} 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 { SignInForm } from '~/components/forms/signin';
|
||||
import { SIGNUP_ERROR_MESSAGES } from '~/components/forms/signup';
|
||||
import { appMetaTags } from '~/utils/meta';
|
||||
|
||||
import type { Route } from './+types/signin';
|
||||
@@ -57,8 +60,14 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
|
||||
returnTo,
|
||||
} = loaderData;
|
||||
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isEmbeddedRedirect, setIsEmbeddedRedirect] = useState(false);
|
||||
|
||||
const errorParam = searchParams.get('error');
|
||||
const signupError = errorParam ? SIGNUP_ERROR_MESSAGES[errorParam] : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
@@ -69,12 +78,18 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
|
||||
|
||||
return (
|
||||
<div className="w-screen max-w-lg px-4">
|
||||
<div className="border-border dark:bg-background z-10 rounded-xl border bg-neutral-100 p-6">
|
||||
<div className="z-10 rounded-xl border border-border bg-neutral-100 p-6 dark:bg-background">
|
||||
{signupError && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertDescription>{_(signupError)}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<h1 className="text-2xl font-semibold">
|
||||
<Trans>Sign in to your account</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
<Trans>Welcome back, we are lucky to have you.</Trans>
|
||||
</p>
|
||||
<hr className="-mx-6 my-4" />
|
||||
@@ -88,7 +103,7 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
|
||||
/>
|
||||
|
||||
{!isEmbeddedRedirect && env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true' && (
|
||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||
<p className="mt-6 text-center text-sm text-muted-foreground">
|
||||
<Trans>
|
||||
Don't have an account?{' '}
|
||||
<Link
|
||||
|
||||
@@ -254,3 +254,4 @@ Here's a markdown table documenting all the provided environment variables:
|
||||
| `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_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`). |
|
||||
|
||||
@@ -59,6 +59,7 @@ 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_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}
|
||||
- NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=${NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS}
|
||||
|
||||
@@ -17,6 +17,7 @@ export const AuthenticationErrorCode = {
|
||||
// TwoFactorMissingSecret: 'TWO_FACTOR_MISSING_SECRET',
|
||||
// TwoFactorMissingCredentials: 'TWO_FACTOR_MISSING_CREDENTIALS',
|
||||
InvalidTwoFactorCode: 'INVALID_TWO_FACTOR_CODE',
|
||||
SignupDisabled: 'SIGNUP_DISABLED',
|
||||
// IncorrectTwoFactorBackupCode: 'INCORRECT_TWO_FACTOR_BACKUP_CODE',
|
||||
// IncorrectIdentityProvider: 'INCORRECT_IDENTITY_PROVIDER',
|
||||
// IncorrectPassword: 'INCORRECT_PASSWORD',
|
||||
|
||||
@@ -3,8 +3,11 @@ import { OAuth2Client, decodeIdToken } from 'arctic';
|
||||
import type { Context } from 'hono';
|
||||
import { deleteCookie } from 'hono/cookie';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { isEmailDomainAllowedForSignup } 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 { env } from '@documenso/lib/utils/env';
|
||||
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 { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
|
||||
@@ -114,6 +117,24 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti
|
||||
return c.redirect(redirectPath, 302);
|
||||
}
|
||||
|
||||
// Check if signups are disabled.
|
||||
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
|
||||
const errorUrl = new URL('/signin', NEXT_PUBLIC_WEBAPP_URL());
|
||||
|
||||
errorUrl.searchParams.set('error', AuthenticationErrorCode.SignupDisabled);
|
||||
|
||||
return c.redirect(errorUrl.toString(), 302);
|
||||
}
|
||||
|
||||
// Check domain restriction for new SSO users.
|
||||
if (!isEmailDomainAllowedForSignup(email)) {
|
||||
const errorUrl = new URL('/signin', NEXT_PUBLIC_WEBAPP_URL());
|
||||
|
||||
errorUrl.searchParams.set('error', AuthenticationErrorCode.SignupDisabled);
|
||||
|
||||
return c.redirect(errorUrl.toString(), 302);
|
||||
}
|
||||
|
||||
// Handle new user.
|
||||
const createdUser = await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.create({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { HTTPException } from 'hono/http-exception';
|
||||
import { DateTime } from 'luxon';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isEmailDomainAllowedForSignup } 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';
|
||||
@@ -122,7 +123,11 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
||||
const is2faEnabled = isTwoFactorAuthenticationEnabled({ user });
|
||||
|
||||
if (is2faEnabled) {
|
||||
const isValid = await validateTwoFactorAuthentication({ backupCode, totpCode, user });
|
||||
const isValid = await validateTwoFactorAuthentication({
|
||||
backupCode,
|
||||
totpCode,
|
||||
user,
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
@@ -178,8 +183,8 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
||||
const requestMetadata = c.get('requestMetadata');
|
||||
|
||||
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
|
||||
throw new AppError('SIGNUP_DISABLED', {
|
||||
message: 'Signups are disabled.',
|
||||
throw new AppError(AuthenticationErrorCode.SignupDisabled, {
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -197,6 +202,12 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
||||
});
|
||||
}
|
||||
|
||||
if (!isEmailDomainAllowedForSignup(email)) {
|
||||
throw new AppError(AuthenticationErrorCode.SignupDisabled, {
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await createUser({ name, email, password, signature }).catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
|
||||
@@ -69,3 +69,40 @@ export const getCookieDomain = () => {
|
||||
|
||||
return url.hostname;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get allowed signup domains from env var.
|
||||
* Returns empty array if not set (meaning all domains allowed).
|
||||
*/
|
||||
export const getAllowedSignupDomains = (): string[] => {
|
||||
const domains = env('NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS');
|
||||
|
||||
if (!domains) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return domains
|
||||
.split(',')
|
||||
.map((d) => d.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if email domain is allowed for signup.
|
||||
* Returns true if no domain restriction is configured.
|
||||
*/
|
||||
export const isEmailDomainAllowedForSignup = (email: string): boolean => {
|
||||
const allowedDomains = getAllowedSignupDomains();
|
||||
|
||||
if (allowedDomains.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const emailDomain = email.toLowerCase().split('@').pop();
|
||||
|
||||
if (!emailDomain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return allowedDomains.includes(emailDomain);
|
||||
};
|
||||
|
||||
Vendored
+1
@@ -74,6 +74,7 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_SMTP_FROM_ADDRESS?: string;
|
||||
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP?: string;
|
||||
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS?: string;
|
||||
|
||||
NEXT_PRIVATE_BROWSERLESS_URL?: string;
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
||||
"NEXT_PUBLIC_DISABLE_SIGNUP",
|
||||
"NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS",
|
||||
"NEXT_PRIVATE_PLAIN_API_KEY",
|
||||
"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT",
|
||||
"NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY",
|
||||
|
||||
Reference in New Issue
Block a user