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
@@ -1,10 +1,3 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import type { Team } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { ConfirmTeamEmailTemplate } from '@documenso/email/templates/confirm-team-email';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
@@ -12,6 +5,11 @@ import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createTokenVerification } from '@documenso/lib/utils/token-verification';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import type { Team } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { createElement } from 'react';
import { z } from 'zod';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { env } from '../../utils/env';
@@ -52,36 +50,35 @@ export const createTeamEmailVerification = async ({
});
}
await prisma.$transaction(
async (tx) => {
const existingTeamEmail = await tx.teamEmail.findFirst({
where: {
email: data.email,
},
const { token, expiresAt } = createTokenVerification({ hours: 1 });
await prisma.$transaction(async (tx) => {
const existingTeamEmail = await tx.teamEmail.findFirst({
where: {
email: data.email,
},
});
if (existingTeamEmail) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'Email already taken by another team.',
});
}
if (existingTeamEmail) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
message: 'Email already taken by another team.',
});
}
await tx.teamEmailVerification.create({
data: {
token,
expiresAt,
email: data.email,
name: data.name,
teamId,
},
});
});
const { token, expiresAt } = createTokenVerification({ hours: 1 });
await tx.teamEmailVerification.create({
data: {
token,
expiresAt,
email: data.email,
name: data.name,
teamId,
},
});
await sendTeamEmailVerificationEmail(data.email, token, team);
},
{ timeout: 30_000 },
);
// Send email outside the transaction to avoid holding a connection
// open during network I/O.
await sendTeamEmailVerificationEmail(data.email, token, team);
} catch (err) {
console.error(err);
@@ -142,9 +139,7 @@ export const sendTeamEmailVerificationEmail = async (email: string, token: strin
await mailer.sendMail({
to: email,
from: senderEmail,
subject: i18n._(
msg`A request to use your email has been initiated by ${team.name} on Documenso`,
),
subject: i18n._(msg`A request to use your email has been initiated by ${team.name} on Documenso`),
html,
text,
});
+4 -19
View File
@@ -1,19 +1,10 @@
import {
OrganisationGroupType,
OrganisationMemberRole,
Prisma,
TeamMemberRole,
} from '@prisma/client';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { OrganisationGroupType, OrganisationMemberRole, Prisma, TeamMemberRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { IS_BILLING_ENABLED } from '../../constants/app';
import {
LOWEST_ORGANISATION_ROLE,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP,
} from '../../constants/organisations';
import { LOWEST_ORGANISATION_ROLE, ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/organisations';
import { TEAM_INTERNAL_GROUPS } from '../../constants/teams';
import { generateDatabaseId } from '../../universal/id';
import { buildOrganisationWhereQuery } from '../../utils/organisations';
@@ -56,13 +47,7 @@ export type CreateTeamOptions = {
}[];
};
export const createTeam = async ({
userId,
teamName,
teamUrl,
organisationId,
inheritMembers,
}: CreateTeamOptions) => {
export const createTeam = async ({ userId, teamName, teamUrl, organisationId, inheritMembers }: CreateTeamOptions) => {
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId,
@@ -8,10 +8,7 @@ export type DeleteTeamEmailVerificationOptions = {
teamId: number;
};
export const deleteTeamEmailVerification = async ({
userId,
teamId,
}: DeleteTeamEmailVerificationOptions) => {
export const deleteTeamEmailVerification = async ({ userId, teamId }: DeleteTeamEmailVerificationOptions) => {
await prisma.team.findFirstOrThrow({
where: buildTeamWhereQuery({
teamId,
@@ -1,12 +1,10 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { mailer } from '@documenso/email/mailer';
import { TeamEmailRemovedTemplate } from '@documenso/email/templates/team-email-removed';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { createElement } from 'react';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { env } from '../../utils/env';
+30 -39
View File
@@ -1,14 +1,12 @@
import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import { OrganisationGroupType, type Team } from '@prisma/client';
import { uniqueBy } from 'remeda';
import { mailer } from '@documenso/email/mailer';
import { TeamDeleteEmailTemplate } from '@documenso/email/templates/team-delete';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { msg } from '@lingui/core/macro';
import { OrganisationGroupType, type Team } from '@prisma/client';
import { createElement } from 'react';
import { uniqueBy } from 'remeda';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
@@ -78,38 +76,35 @@ export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
(member) => member.id,
);
await prisma.$transaction(
async (tx) => {
await tx.team.delete({
where: {
id: teamId,
},
});
await prisma.$transaction(async (tx) => {
await tx.team.delete({
where: {
id: teamId,
},
});
// Purge all internal organisation groups that have no teams.
await tx.organisationGroup.deleteMany({
where: {
type: OrganisationGroupType.INTERNAL_TEAM,
teamGroups: {
none: {},
},
// Purge all internal organisation groups that have no teams.
await tx.organisationGroup.deleteMany({
where: {
type: OrganisationGroupType.INTERNAL_TEAM,
teamGroups: {
none: {},
},
});
},
});
});
await jobs.triggerJob({
name: 'send.team-deleted.email',
payload: {
team: {
name: team.name,
url: team.url,
},
members: membersToNotify,
organisationId: team.organisationId,
},
});
await jobs.triggerJob({
name: 'send.team-deleted.email',
payload: {
team: {
name: team.name,
url: team.url,
},
members: membersToNotify,
organisationId: team.organisationId,
},
{ timeout: 30_000 },
);
});
};
type SendTeamDeleteEmailOptions = {
@@ -118,11 +113,7 @@ type SendTeamDeleteEmailOptions = {
organisationId: string;
};
export const sendTeamDeleteEmail = async ({
email,
team,
organisationId,
}: SendTeamDeleteEmailOptions) => {
export const sendTeamDeleteEmail = async ({ email, team, organisationId }: SendTeamDeleteEmailOptions) => {
const template = createElement(TeamDeleteEmailTemplate, {
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
@@ -1,8 +1,7 @@
import { prisma } from '@documenso/prisma';
import type { OrganisationMember } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { P, match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import { match, P } from 'ts-pattern';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { FindResultResponse } from '../../types/search-params';
@@ -144,14 +143,10 @@ export const findTeamMembers = async ({
avatarImageId: member.user.avatarImageId,
// Filter teamGroups to only include the current team
teamRole: getHighestTeamRoleInGroup(
member.organisationGroupMembers.flatMap(({ group }) =>
group.teamGroups.filter((tg) => tg.teamId === teamId),
),
member.organisationGroupMembers.flatMap(({ group }) => group.teamGroups.filter((tg) => tg.teamId === teamId)),
),
teamRoleGroupType: member.organisationGroupMembers[0].group.type,
organisationRole: getHighestOrganisationRoleInGroup(
member.organisationGroupMembers.flatMap(({ group }) => group),
),
organisationRole: getHighestOrganisationRoleInGroup(member.organisationGroupMembers.flatMap(({ group }) => group)),
}));
return {
+1 -2
View File
@@ -1,8 +1,7 @@
import { prisma } from '@documenso/prisma';
import type { Team } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import type { FindResultResponse } from '../../types/search-params';
import { getHighestTeamRoleInGroup } from '../../utils/teams';
@@ -83,10 +83,7 @@ type GetMemberOrganisationRoleOptions = {
/**
* Returns the highest Organisation of a given organisation member
*/
export const getMemberOrganisationRole = async ({
organisationId,
reference,
}: GetMemberOrganisationRoleOptions) => {
export const getMemberOrganisationRole = async ({ organisationId, reference }: GetMemberOrganisationRoleOptions) => {
const organisation = await prisma.organisation.findFirst({
where: {
id: organisationId,
@@ -23,13 +23,7 @@ export const getTeamEmailByEmail = async ({ email }: GetTeamEmailByEmailOptions)
});
};
export const getTeamWithEmail = async ({
userId,
teamUrl,
}: {
userId: number;
teamUrl: string;
}) => {
export const getTeamWithEmail = async ({ userId, teamUrl }: { userId: number; teamUrl: string }) => {
return await prisma.team.findFirstOrThrow({
where: {
...buildTeamWhereQuery({ teamId: undefined, userId }),
@@ -13,10 +13,7 @@ export type GetTeamMembersOptions = {
/**
* Get all team members for a given team.
*/
export const getTeamMembers = async ({
userId,
teamId,
}: GetTeamMembersOptions): Promise<TGetTeamMembersResponse> => {
export const getTeamMembers = async ({ userId, teamId }: GetTeamMembersOptions): Promise<TGetTeamMembersResponse> => {
const teamMembers = await prisma.organisationMember.findMany({
where: {
organisationGroupMembers: {
@@ -1,6 +1,5 @@
import type { TeamProfile } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import type { TeamProfile } from '@prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { buildTeamWhereQuery } from '../../utils/teams';
@@ -39,6 +39,8 @@ export const getTeamSettings = async ({ userId, teamId }: GetTeamSettingsOptions
teamSettings.brandingLogo = organisationSettings.brandingLogo;
teamSettings.brandingUrl = organisationSettings.brandingUrl;
teamSettings.brandingCompanyDetails = organisationSettings.brandingCompanyDetails;
teamSettings.brandingColors = organisationSettings.brandingColors;
teamSettings.brandingCss = organisationSettings.brandingCss;
}
return extractDerivedTeamSettings(organisationSettings, teamSettings);
+2 -12
View File
@@ -1,11 +1,7 @@
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import {
buildTeamWhereQuery,
extractDerivedTeamSettings,
getHighestTeamRoleInGroup,
} from '../../utils/teams';
import { buildTeamWhereQuery, extractDerivedTeamSettings, getHighestTeamRoleInGroup } from '../../utils/teams';
export type GetTeamByIdOptions = {
userId: number;
@@ -39,13 +35,7 @@ export const getTeamByUrl = async ({ userId, teamUrl }: GetTeamByUrlOptions) =>
/**
* Get a team by its ID or URL.
*/
export const getTeam = async ({
teamReference,
userId,
}: {
teamReference: number | string;
userId: number;
}) => {
export const getTeam = async ({ teamReference, userId }: { teamReference: number | string; userId: number }) => {
const team = await prisma.team.findFirst({
where: {
...buildTeamWhereQuery({ teamId: undefined, userId }),
+1 -2
View File
@@ -1,8 +1,7 @@
import { z } from 'zod';
import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/generated/types';
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import { z } from 'zod';
import { buildTeamWhereQuery, getHighestTeamRoleInGroup } from '../../utils/teams';
@@ -0,0 +1,43 @@
import { prisma } from '@documenso/prisma';
import type { TeamGroup } from '@prisma/client';
export type GetUserTeamIdsOptions = {
userId: number;
};
/**
* Pre-resolve all team groups a user has access to via their organisation group memberships,
* keyed by team ID.
*
* This is significantly cheaper than joining team groups inline in a Prisma `findMany`
* because it avoids deep EXISTS subqueries and redundant LEFT JOINs per row.
*/
export const getUserTeamGroups = async ({ userId }: GetUserTeamIdsOptions): Promise<Map<number, TeamGroup[]>> => {
const teamGroups = await prisma.teamGroup.findMany({
where: {
organisationGroup: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
});
const map = new Map<number, TeamGroup[]>();
for (const tg of teamGroups) {
const existing = map.get(tg.teamId);
if (existing) {
existing.push(tg);
} else {
map.set(tg.teamId, [tg]);
}
}
return map;
};
@@ -35,30 +35,27 @@ export const resendTeamEmailVerification = async ({
});
}
await prisma.$transaction(
async (tx) => {
const { emailVerification } = team;
const { emailVerification } = team;
if (!emailVerification) {
throw new AppError('VerificationNotFound', {
message: 'No team email verification exists for this team.',
});
}
if (!emailVerification) {
throw new AppError('VerificationNotFound', {
message: 'No team email verification exists for this team.',
});
}
const { token, expiresAt } = createTokenVerification({ hours: 1 });
const { token, expiresAt } = createTokenVerification({ hours: 1 });
await tx.teamEmailVerification.update({
where: {
teamId,
},
data: {
token,
expiresAt,
},
});
await sendTeamEmailVerificationEmail(emailVerification.email, token, team);
await prisma.teamEmailVerification.update({
where: {
teamId,
},
{ timeout: 30_000 },
);
data: {
token,
expiresAt,
},
});
// Send email outside any transaction to avoid holding a connection
// open during network I/O.
await sendTeamEmailVerificationEmail(emailVerification.email, token, team);
};
@@ -12,11 +12,7 @@ export type UpdateTeamEmailOptions = {
};
};
export const updateTeamEmail = async ({
userId,
teamId,
data,
}: UpdateTeamEmailOptions): Promise<void> => {
export const updateTeamEmail = async ({ userId, teamId, data }: UpdateTeamEmailOptions): Promise<void> => {
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery({
teamId,
@@ -11,11 +11,7 @@ export type UpdatePublicProfileOptions = {
};
};
export const updateTeamPublicProfile = async ({
userId,
teamId,
data,
}: UpdatePublicProfileOptions) => {
export const updateTeamPublicProfile = async ({ userId, teamId, data }: UpdatePublicProfileOptions) => {
return await prisma.team.update({
where: buildTeamWhereQuery({ teamId, userId }),
data: {
+2 -3
View File
@@ -1,9 +1,8 @@
import { Prisma } from '@prisma/client';
import { z } from 'zod';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { Prisma } from '@prisma/client';
import { z } from 'zod';
import { buildTeamWhereQuery } from '../../utils/teams';