chore: merge main, resolve biome formatting conflicts

Merge origin/main into feat/external-2fa-codes. Resolve formatting
conflicts caused by biome rollout; preserve both feature streams:
PR's external 2FA token + signing-session 2FA proof additions plus
main's RateLimit/RecipientExpired/signingReminders/date-auto-insert.

In complete-document-with-token.ts, drop the duplicate early
field-fetching block introduced when main moved that logic later
with date auto-insert support; keep the EXTERNAL_TWO_FACTOR_AUTH
check using derivedRecipientActionAuth.
This commit is contained in:
ephraimduncan
2026-05-12 11:46:11 +00:00
parent 9194884fbe
commit 138d663c25
1959 changed files with 93488 additions and 47038 deletions
+17 -4
View File
@@ -1,8 +1,7 @@
import { prisma } from '@documenso/prisma';
import { hash } from '@node-rs/bcrypt';
import type { User } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { SALT_ROUNDS } from '../../constants/auth';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { createPersonalOrganisation } from '../organisation/create-organisation';
@@ -60,13 +59,27 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
return user;
};
export type OnCreateUserHookOptions = {
/**
* When true, do not create a "Personal Organisation" for the new user.
* Used by the Organisation SSO signup path, where the user is intended
* to operate inside the SSO organisation rather than a personal space.
*
* Defaults to false — preserves the historical behaviour of creating a
* personal organisation for every new user.
*/
skipPersonalOrganisation?: boolean;
};
/**
* Should be run after a user is created, example during email password signup or google sign in.
*
* @returns User
*/
export const onCreateUserHook = async (user: User) => {
await createPersonalOrganisation({ userId: user.id });
export const onCreateUserHook = async (user: User, options: OnCreateUserHookOptions = {}) => {
if (!options.skipPersonalOrganisation) {
await createPersonalOrganisation({ userId: user.id });
}
return user;
};
@@ -1,6 +1,5 @@
import type { UserSecurityAuditLog, UserSecurityAuditLogType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import type { UserSecurityAuditLog, UserSecurityAuditLogType } from '@prisma/client';
import type { FindResultResponse } from '../../types/search-params';
@@ -1,8 +1,7 @@
import { prisma } from '@documenso/prisma';
import crypto from 'crypto';
import { prisma } from '@documenso/prisma';
import { ONE_DAY } from '../../constants/time';
import { ONE_HOUR } from '../../constants/time';
import { sendForgotPassword } from '../auth/send-forgot-password';
export const forgotPassword = async ({ email }: { email: string }) => {
@@ -41,7 +40,7 @@ export const forgotPassword = async ({ email }: { email: string }) => {
await prisma.passwordResetToken.create({
data: {
token,
expiry: new Date(Date.now() + ONE_DAY),
expiry: new Date(Date.now() + ONE_HOUR),
userId: user.id,
},
});
@@ -1,6 +1,5 @@
import { EnvelopeType, Prisma } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { EnvelopeType, Prisma } from '@prisma/client';
type GetAllUsersProps = {
username: string;
@@ -9,12 +8,7 @@ type GetAllUsersProps = {
perPage: number;
};
export const findUsers = async ({
username = '',
email = '',
page = 1,
perPage = 10,
}: GetAllUsersProps) => {
export const findUsers = async ({ username = '', email = '', page = 1, perPage = 10 }: GetAllUsersProps) => {
const whereClause = Prisma.validator<Prisma.UserWhereInput>()({
OR: [
{
@@ -1,8 +1,7 @@
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
export const getCompletedDocumentsMonthly = async () => {
const qb = kyselyPrisma.$kysely
.selectFrom('Envelope')
@@ -31,6 +30,4 @@ export const getCompletedDocumentsMonthly = async () => {
}));
};
export type GetCompletedDocumentsMonthlyResult = Awaited<
ReturnType<typeof getCompletedDocumentsMonthly>
>;
export type GetCompletedDocumentsMonthlyResult = Awaited<ReturnType<typeof getCompletedDocumentsMonthly>>;
@@ -6,9 +6,7 @@ export type getMostRecentEmailVerificationTokenOptions = {
userId: number;
};
export const getMostRecentEmailVerificationToken = async ({
userId,
}: getMostRecentEmailVerificationTokenOptions) => {
export const getMostRecentEmailVerificationToken = async ({ userId }: getMostRecentEmailVerificationTokenOptions) => {
return await prisma.verificationToken.findFirst({
where: {
userId,
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DateTime } from 'luxon';
export const getSignerConversionMonthly = async () => {
const qb = kyselyPrisma.$kysely
@@ -29,6 +28,4 @@ export const getSignerConversionMonthly = async () => {
}));
};
export type GetSignerConversionMonthlyResult = Awaited<
ReturnType<typeof getSignerConversionMonthly>
>;
export type GetSignerConversionMonthlyResult = Awaited<ReturnType<typeof getSignerConversionMonthly>>;
@@ -0,0 +1,24 @@
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
export interface GetUserByResetTokenOptions {
token: string;
}
export const getUserByResetToken = async ({ token }: GetUserByResetTokenOptions) => {
const result = await prisma.passwordResetToken.findFirst({
where: {
token,
},
include: {
user: true,
},
});
if (!result || !result.user) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
return result.user;
};
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { kyselyPrisma, sql } from '@documenso/prisma';
import { DateTime } from 'luxon';
export const getUserMonthlyGrowth = async () => {
const qb = kyselyPrisma.$kysely
@@ -1,8 +1,7 @@
import { prisma } from '@documenso/prisma';
import { compare, hash } from '@node-rs/bcrypt';
import { UserSecurityAuditLogType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { SALT_ROUNDS } from '../../constants/auth';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { jobsClient } from '../../jobs/client';
@@ -77,13 +76,13 @@ export const resetPassword = async ({ token, password, requestMetadata }: ResetP
ipAddress: requestMetadata?.ipAddress,
},
});
});
await jobsClient.triggerJob({
name: 'send.password.reset.success.email',
payload: {
userId: foundToken.userId,
},
});
await jobsClient.triggerJob({
name: 'send.password.reset.success.email',
payload: {
userId: foundToken.userId,
},
});
return {
@@ -1,8 +1,7 @@
import { prisma } from '@documenso/prisma';
import crypto from 'crypto';
import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
import { USER_SIGNUP_VERIFICATION_TOKEN_IDENTIFIER } from '../../constants/email';
import { ONE_HOUR } from '../../constants/time';
import { sendConfirmationEmail } from '../auth/send-confirmation-email';
@@ -10,10 +9,7 @@ import { getMostRecentEmailVerificationToken } from './get-most-recent-email-ver
type SendConfirmationTokenOptions = { email: string; force?: boolean };
export const sendConfirmationToken = async ({
email,
force = false,
}: SendConfirmationTokenOptions) => {
export const sendConfirmationToken = async ({ email, force = false }: SendConfirmationTokenOptions) => {
const token = crypto.randomBytes(20).toString('hex');
const user = await prisma.user.findFirst({
@@ -1,9 +1,27 @@
import { prisma } from '@documenso/prisma';
const LEGACY_DELETED_ACCOUNT_EMAIL = 'deleted-account@documenso.com';
export const deletedServiceAccountEmail = () => {
try {
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.NEXT_PRIVATE_DELETED_SERVICE_ACCOUNT_EMAIL) {
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PRIVATE_DELETED_SERVICE_ACCOUNT_EMAIL;
}
const { hostname } = new URL(process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000');
return `deleted-account@${hostname}`;
} catch (error) {
return LEGACY_DELETED_ACCOUNT_EMAIL;
}
};
export const deletedAccountServiceAccount = async () => {
const serviceAccount = await prisma.user.findFirst({
where: {
email: 'deleted-account@documenso.com',
email: deletedServiceAccountEmail(),
},
select: {
id: true,
@@ -22,10 +40,23 @@ export const deletedAccountServiceAccount = async () => {
});
if (!serviceAccount) {
throw new Error(
'Deleted account service account not found, have you ran the appropriate migrations?',
);
throw new Error('Deleted account service account not found, have you ran the appropriate migrations?');
}
return serviceAccount;
};
export const migrateDeletedAccountServiceAccount = async () => {
if (deletedServiceAccountEmail() !== LEGACY_DELETED_ACCOUNT_EMAIL) {
console.log(`Migrating deleted account service account to new email: ${deletedServiceAccountEmail()}`);
await prisma.user.updateMany({
where: {
email: LEGACY_DELETED_ACCOUNT_EMAIL,
},
data: {
email: deletedServiceAccountEmail(),
},
});
}
};
@@ -0,0 +1,34 @@
import { prisma } from '@documenso/prisma';
const LEGACY_SERVICE_ACCOUNT_EMAIL = 'serviceaccount@documenso.com';
export const legacyServiceAccountEmail = () => {
try {
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.NEXT_PRIVATE_LEGACY_SERVICE_ACCOUNT_EMAIL) {
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PRIVATE_LEGACY_SERVICE_ACCOUNT_EMAIL;
}
const { hostname } = new URL(process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000');
return `serviceaccount@${hostname}`;
} catch (error) {
return LEGACY_SERVICE_ACCOUNT_EMAIL;
}
};
export const migrateLegacyServiceAccount = async () => {
if (legacyServiceAccountEmail() !== LEGACY_SERVICE_ACCOUNT_EMAIL) {
console.log(`Migrating legacy service account to new email: ${legacyServiceAccountEmail()}`);
await prisma.user.updateMany({
where: {
email: LEGACY_SERVICE_ACCOUNT_EMAIL,
},
data: {
email: legacyServiceAccountEmail(),
},
});
}
};
@@ -1,9 +1,8 @@
import { compare, hash } from '@node-rs/bcrypt';
import { UserSecurityAuditLogType } from '@prisma/client';
import { SALT_ROUNDS } from '@documenso/lib/constants/auth';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import { compare, hash } from '@node-rs/bcrypt';
import { UserSecurityAuditLogType } from '@prisma/client';
import { AppError } from '../../errors/app-error';
@@ -14,12 +13,7 @@ export type UpdatePasswordOptions = {
requestMetadata?: RequestMetadata;
};
export const updatePassword = async ({
userId,
password,
currentPassword,
requestMetadata,
}: UpdatePasswordOptions) => {
export const updatePassword = async ({ userId, password, currentPassword, requestMetadata }: UpdatePasswordOptions) => {
// Existence check
const user = await prisma.user.findFirstOrThrow({
where: {
@@ -54,6 +48,12 @@ export const updatePassword = async ({
},
});
await tx.passwordResetToken.deleteMany({
where: {
userId,
},
});
return await tx.user.update({
where: {
id: userId,
@@ -1,6 +1,5 @@
import { UserSecurityAuditLogType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@prisma/client';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
@@ -11,12 +10,7 @@ export type UpdateProfileOptions = {
requestMetadata?: RequestMetadata;
};
export const updateProfile = async ({
userId,
name,
signature,
requestMetadata,
}: UpdateProfileOptions) => {
export const updateProfile = async ({ userId, name, signature, requestMetadata }: UpdateProfileOptions) => {
// Existence check
await prisma.user.findFirstOrThrow({
where: {
+3 -10
View File
@@ -1,11 +1,7 @@
import { prisma } from '@documenso/prisma';
import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
import {
EMAIL_VERIFICATION_STATE,
USER_SIGNUP_VERIFICATION_TOKEN_IDENTIFIER,
} from '../../constants/email';
import { EMAIL_VERIFICATION_STATE, USER_SIGNUP_VERIFICATION_TOKEN_IDENTIFIER } from '../../constants/email';
import { jobsClient } from '../../jobs/client';
export type VerifyEmailProps = {
@@ -50,10 +46,7 @@ export const verifyEmail = async ({ token }: VerifyEmailProps) => {
});
// If there isn't a recent token or it's older than 1 hour, send a new token
if (
!mostRecentToken ||
DateTime.now().minus({ hours: 1 }).toJSDate() > mostRecentToken.createdAt
) {
if (!mostRecentToken || DateTime.now().minus({ hours: 1 }).toJSDate() > mostRecentToken.createdAt) {
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {