This commit is contained in:
David Nguyen
2025-01-31 14:09:02 +11:00
parent f7a98180d7
commit d7d0fca501
146 changed files with 1250 additions and 1263 deletions

View File

@ -3,7 +3,6 @@ import { base32 } from '@scure/base';
import crypto from 'crypto';
import { createTOTPKeyURI } from 'oslo/otp';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { prisma } from '@documenso/prisma';
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
@ -21,7 +20,7 @@ export const setupTwoFactorAuthentication = async ({
const key = DOCUMENSO_ENCRYPTION_KEY;
if (!key) {
throw new Error(ErrorCode.MISSING_ENCRYPTION_KEY);
throw new Error('MISSING_ENCRYPTION_KEY');
}
const secret = crypto.randomBytes(10);

View File

@ -1,6 +1,6 @@
import type { User } from '@prisma/client';
import { ErrorCode } from '../../next-auth/error-codes';
import { AppError } from '../../errors/app-error';
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
import { verifyBackupCode } from './verify-backup-code';
@ -16,11 +16,11 @@ export const validateTwoFactorAuthentication = async ({
user,
}: ValidateTwoFactorAuthenticationOptions) => {
if (!user.twoFactorEnabled) {
throw new Error(ErrorCode.TWO_FACTOR_SETUP_REQUIRED);
throw new AppError('TWO_FACTOR_SETUP_REQUIRED');
}
if (!user.twoFactorSecret) {
throw new Error(ErrorCode.TWO_FACTOR_MISSING_SECRET);
throw new AppError('TWO_FACTOR_MISSING_SECRET');
}
if (totpCode) {
@ -31,5 +31,5 @@ export const validateTwoFactorAuthentication = async ({
return await verifyBackupCode({ user, backupCode });
}
throw new Error(ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS);
throw new AppError('TWO_FACTOR_MISSING_CREDENTIALS');
};

View File

@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendConfirmationEmailProps {
@ -15,8 +16,8 @@ export interface SendConfirmationEmailProps {
}
export const sendConfirmationEmail = async ({ userId }: SendConfirmationEmailProps) => {
const NEXT_PRIVATE_SMTP_FROM_NAME = process.env.NEXT_PRIVATE_SMTP_FROM_NAME;
const NEXT_PRIVATE_SMTP_FROM_ADDRESS = process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS;
const NEXT_PRIVATE_SMTP_FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME');
const NEXT_PRIVATE_SMTP_FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS');
const user = await prisma.user.findFirstOrThrow({
where: {

View File

@ -8,6 +8,7 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendForgotPasswordOptions {
@ -55,8 +56,8 @@ export const sendForgotPassword = async ({ userId }: SendForgotPasswordOptions)
name: user.name || '',
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Forgot Password?`),
html,

View File

@ -5,6 +5,7 @@ import { ResetPasswordTemplate } from '@documenso/email/templates/reset-password
import { prisma } from '@documenso/prisma';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
export interface SendResetPasswordOptions {
@ -37,8 +38,8 @@ export const sendResetPassword = async ({ userId }: SendResetPasswordOptions) =>
name: user.name || '',
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: 'Password Reset Success!',
html,

View File

@ -14,6 +14,7 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { env } from '../../utils/env';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -113,8 +114,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
},
],
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Signing Complete!`),
html,
@ -190,8 +191,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
},
],
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject:
isDirectTemplate && document.documentMeta?.subject

View File

@ -10,6 +10,7 @@ import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -79,8 +80,8 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
name: name || '',
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Document Deleted!`),
html,

View File

@ -9,6 +9,7 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -90,8 +91,8 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
name,
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Waiting for others to complete signing.`),
html,

View File

@ -3,6 +3,7 @@ import type { Browser } from 'playwright';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
import { env } from '../../utils/env';
import { encryptSecondaryData } from '../crypto/encrypt';
export type GetCertificatePdfOptions = {
@ -21,10 +22,10 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
let browser: Browser;
if (process.env.NEXT_PRIVATE_BROWSERLESS_URL) {
if (env('NEXT_PRIVATE_BROWSERLESS_URL')) {
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
browser = await chromium.connectOverCDP(process.env.NEXT_PRIVATE_BROWSERLESS_URL);
browser = await chromium.connectOverCDP(env('NEXT_PRIVATE_BROWSERLESS_URL'));
} else {
browser = await chromium.launch();
}

View File

@ -25,15 +25,12 @@ import {
ZRadioFieldMeta,
ZTextFieldMeta,
} from '../../types/field-meta';
import { env } from '../../utils/env';
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
const fontCaveat = await fetch(process.env.FONT_CAVEAT_URI).then(async (res) =>
res.arrayBuffer(),
);
const fontCaveat = await fetch(env('FONT_CAVEAT_URI')).then(async (res) => res.arrayBuffer());
const fontNoto = await fetch(process.env.FONT_NOTO_SANS_URI).then(async (res) =>
res.arrayBuffer(),
);
const fontNoto = await fetch(env('FONT_NOTO_SANS_URI')).then(async (res) => res.arrayBuffer());
const isSignatureField = isSignatureFieldType(field.type);

View File

@ -1,8 +1,10 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { Redis } from '@upstash/redis';
import { env } from '../../utils/env';
// !: We're null coalescing here because we don't want local builds to fail.
export const redis = new Redis({
url: process.env.NEXT_PRIVATE_REDIS_URL ?? '',
token: process.env.NEXT_PRIVATE_REDIS_TOKEN ?? '',
url: env('NEXT_PRIVATE_REDIS_URL') ?? '',
token: env('NEXT_PRIVATE_REDIS_TOKEN') ?? '',
});

View File

@ -1,7 +1,9 @@
/// <reference types="./stripe.d.ts" />
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY ?? '', {
import { env } from '../../utils/env';
export const stripe = new Stripe(env('NEXT_PRIVATE_STRIPE_API_KEY') ?? '', {
apiVersion: '2022-11-15',
typescript: true,
});

View File

@ -15,6 +15,7 @@ import { createTokenVerification } from '@documenso/lib/utils/token-verification
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -122,7 +123,7 @@ export const sendTeamEmailVerificationEmail = async (
teamGlobalSettings?: TeamGlobalSettings | null;
},
) => {
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const assetBaseUrl = env('NEXT_PUBLIC_WEBAPP_URL') || 'http://localhost:3000';
const template = createElement(ConfirmTeamEmailTemplate, {
assetBaseUrl,

View File

@ -10,6 +10,7 @@ import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
@ -69,7 +70,7 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
});
try {
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
const assetBaseUrl = env('NEXT_PUBLIC_WEBAPP_URL') || 'http://localhost:3000';
const template = createElement(TeamEmailRemovedTemplate, {
assetBaseUrl,

View File

@ -44,6 +44,7 @@ import {
createRecipientAuthOptions,
extractDocumentAuthMethods,
} from '../../utils/document-auth';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
import { formatDocumentsPath } from '../../utils/teams';
@ -582,8 +583,8 @@ export const createDocumentFromDirectTemplate = async ({
},
],
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
},
subject: i18n._(msg`Document created from direct template`),
html,

View File

@ -1,5 +1,5 @@
import { hash } from '@node-rs/bcrypt';
import { IdentityProvider, TeamMemberInviteStatus } from '@prisma/client';
import { TeamMemberInviteStatus } from '@prisma/client';
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
import { updateSubscriptionItemQuantity } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
@ -39,22 +39,35 @@ export const createUser = async ({ name, email, password, signature, url }: Crea
});
if (urlExists) {
throw new AppError(AppErrorCode.PROFILE_URL_TAKEN, {
throw new AppError('PROFILE_URL_TAKEN', {
message: 'Profile username is taken',
userMessage: 'The profile username is already taken',
});
}
}
const user = await prisma.user.create({
data: {
name,
email: email.toLowerCase(),
password: hashedPassword,
signature,
identityProvider: IdentityProvider.DOCUMENSO,
url,
},
const user = await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
name,
email: email.toLowerCase(),
password: hashedPassword, // Todo: Drop password.
signature,
url,
},
});
await tx.account.create({
data: {
userId: user.id,
type: 'emailPassword', // Todo
provider: 'DOCUMENSO', // Todo: Enums
providerAccountId: user.id.toString(),
password: hashedPassword,
},
});
return user;
});
const acceptedTeamInvites = await prisma.teamMemberInvite.findMany({

View File

@ -26,7 +26,10 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
});
if (!verificationToken) {
return EMAIL_VERIFICATION_STATE.NOT_FOUND;
return {
state: EMAIL_VERIFICATION_STATE.NOT_FOUND,
userId: null,
};
}
// check if the token is valid or expired
@ -55,11 +58,17 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
});
}
return EMAIL_VERIFICATION_STATE.EXPIRED;
return {
state: EMAIL_VERIFICATION_STATE.EXPIRED,
userId: null,
};
}
if (verificationToken.completed) {
return EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED;
return {
state: EMAIL_VERIFICATION_STATE.ALREADY_VERIFIED,
userId: null,
};
}
const [updatedUser] = await prisma.$transaction([
@ -94,5 +103,8 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
throw new Error('Something went wrong while verifying your email. Please try again.');
}
return EMAIL_VERIFICATION_STATE.VERIFIED;
return {
state: EMAIL_VERIFICATION_STATE.VERIFIED,
userId: updatedUser.id,
};
};