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,7 +1,6 @@
import { OrganisationType } from '@prisma/client';
import { createOrganisation } from '@documenso/lib/server-only/organisation/create-organisation';
import { internalClaims } from '@documenso/lib/types/subscription';
import { OrganisationType } from '@prisma/client';
import { adminProcedure } from '../trpc';
import {
@@ -38,19 +38,19 @@ export const createStripeCustomerRoute = adminProcedure
throw new AppError(AppErrorCode.NOT_FOUND);
}
await prisma.$transaction(async (tx) => {
const stripeCustomer = await createCustomer({
name: organisation.name,
email: organisation.owner.email,
});
// Create Stripe customer outside a transaction to avoid holding a
// connection open during the external API call.
const stripeCustomer = await createCustomer({
name: organisation.name,
email: organisation.owner.email,
});
await tx.organisation.update({
where: {
id: organisationId,
},
data: {
customerId: stripeCustomer.id,
},
});
await prisma.organisation.update({
where: {
id: organisationId,
},
data: {
customerId: stripeCustomer.id,
},
});
});
@@ -1,6 +1,5 @@
import { z } from 'zod';
import { ZClaimFlagsSchema } from '@documenso/lib/types/subscription';
import { z } from 'zod';
export const ZCreateSubscriptionClaimRequestSchema = z.object({
name: z.string().min(1),
@@ -2,10 +2,7 @@ import { adminSuperDeleteDocument } from '@documenso/lib/server-only/admin/admin
import { sendDeleteEmail } from '@documenso/lib/server-only/document/send-delete-email';
import { adminProcedure } from '../trpc';
import {
ZDeleteDocumentRequestSchema,
ZDeleteDocumentResponseSchema,
} from './delete-document.types';
import { ZDeleteDocumentRequestSchema, ZDeleteDocumentResponseSchema } from './delete-document.types';
export const deleteDocumentRoute = adminProcedure
.input(ZDeleteDocumentRequestSchema)
@@ -0,0 +1,123 @@
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { prisma } from '@documenso/prisma';
import { OrganisationMemberInviteStatus } from '@prisma/client';
import { adminProcedure } from '../trpc';
import {
ZDeleteAdminOrganisationMemberRequestSchema,
ZDeleteAdminOrganisationMemberResponseSchema,
} from './delete-organisation-member.types';
export const deleteAdminOrganisationMemberRoute = adminProcedure
.input(ZDeleteAdminOrganisationMemberRequestSchema)
.output(ZDeleteAdminOrganisationMemberResponseSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId, organisationMemberId } = input;
ctx.logger.info({
input: {
organisationId,
organisationMemberId,
},
});
const organisation = await prisma.organisation.findUnique({
where: {
id: organisationId,
},
include: {
subscription: true,
organisationClaim: true,
teams: {
select: {
id: true,
},
},
members: {
select: {
id: true,
userId: true,
},
},
invites: {
where: {
status: OrganisationMemberInviteStatus.PENDING,
},
select: {
id: true,
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation not found',
});
}
const memberToDelete = organisation.members.find((member) => member.id === organisationMemberId);
if (!memberToDelete) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Member not found in this organisation',
});
}
if (memberToDelete.userId === organisation.ownerUserId) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Cannot remove the organisation owner. Transfer ownership first.',
});
}
const newMemberCount = organisation.members.length + organisation.invites.length - 1;
// Removing a member is a reducing operation, so we don't gate it on the
// subscription being present. Sync Stripe only when one exists.
if (organisation.subscription) {
await syncMemberCountWithStripeSeatPlan(
organisation.subscription,
organisation.organisationClaim,
newMemberCount,
);
}
const teamIds = organisation.teams.map((team) => team.id);
await prisma.$transaction(async (tx) => {
// Removing an OrganisationMember cascades the user out of every team in
// the org via OrganisationGroupMember, but their authored Envelope rows
// still reference them. Reassign those to the org owner so they remain
// reachable after the member loses access (mirrors delete-user.ts).
if (teamIds.length > 0) {
await tx.envelope.updateMany({
where: {
userId: memberToDelete.userId,
teamId: {
in: teamIds,
},
},
data: {
userId: organisation.ownerUserId,
},
});
}
await tx.organisationMember.delete({
where: {
id: organisationMemberId,
organisationId,
},
});
});
await jobs.triggerJob({
name: 'send.organisation-member-left.email',
payload: {
organisationId,
memberUserId: memberToDelete.userId,
},
});
});
@@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZDeleteAdminOrganisationMemberRequestSchema = z.object({
organisationId: z.string().min(1),
organisationMemberId: z.string().min(1),
});
export const ZDeleteAdminOrganisationMemberResponseSchema = z.void();
export type TDeleteAdminOrganisationMemberRequest = z.infer<typeof ZDeleteAdminOrganisationMemberRequestSchema>;
export type TDeleteAdminOrganisationMemberResponse = z.infer<typeof ZDeleteAdminOrganisationMemberResponseSchema>;
@@ -0,0 +1,101 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { OrganisationGroupType } from '@prisma/client';
import { adminProcedure } from '../trpc';
import { ZDeleteAdminTeamMemberRequestSchema, ZDeleteAdminTeamMemberResponseSchema } from './delete-team-member.types';
export const deleteAdminTeamMemberRoute = adminProcedure
.input(ZDeleteAdminTeamMemberRequestSchema)
.output(ZDeleteAdminTeamMemberResponseSchema)
.mutation(async ({ ctx, input }) => {
const { teamId, memberId } = input;
ctx.logger.info({
input: {
teamId,
memberId,
},
});
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
include: {
organisation: {
select: {
ownerUserId: true,
},
},
teamGroups: {
where: {
organisationGroup: {
type: OrganisationGroupType.INTERNAL_TEAM,
organisationGroupMembers: {
some: {
organisationMember: {
id: memberId,
},
},
},
},
},
},
},
});
if (!team) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team not found',
});
}
const teamGroupToRemoveMemberFrom = team.teamGroups[0];
if (!teamGroupToRemoveMemberFrom) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Member is not directly assigned to this team. Inherited members cannot be removed here.',
});
}
const member = await prisma.organisationMember.findUnique({
where: {
id: memberId,
},
select: {
userId: true,
},
});
if (!member) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Member not found',
});
}
await prisma.$transaction(async (tx) => {
// Removing a user from a single team drops their INTERNAL_TEAM
// OrganisationGroupMember link, but Envelope rows they authored in this
// team still point at their userId. Reassign to the org owner so those
// envelopes remain reachable after the member loses team access.
await tx.envelope.updateMany({
where: {
userId: member.userId,
teamId,
},
data: {
userId: team.organisation.ownerUserId,
},
});
await tx.organisationGroupMember.delete({
where: {
organisationMemberId_groupId: {
organisationMemberId: memberId,
groupId: teamGroupToRemoveMemberFrom.organisationGroupId,
},
},
});
});
});
@@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZDeleteAdminTeamMemberRequestSchema = z.object({
teamId: z.number().min(1),
memberId: z.string().min(1),
});
export const ZDeleteAdminTeamMemberResponseSchema = z.void();
export type TDeleteAdminTeamMemberRequest = z.infer<typeof ZDeleteAdminTeamMemberRequestSchema>;
export type TDeleteAdminTeamMemberResponse = z.infer<typeof ZDeleteAdminTeamMemberResponseSchema>;
@@ -0,0 +1,81 @@
import { PDF_SIZE_A4_72PPI } from '@documenso/lib/constants/pdf';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { generateAuditLogPdf } from '@documenso/lib/server-only/pdf/generate-audit-log-pdf';
import { unsafeBuildEnvelopeIdQuery } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { EnvelopeType } from '@prisma/client';
import { adminProcedure } from '../trpc';
import {
ZAdminDownloadDocumentAuditLogsRequestSchema,
ZAdminDownloadDocumentAuditLogsResponseSchema,
} from './download-document-audit-logs.types';
export const downloadDocumentAuditLogsRoute = adminProcedure
.input(ZAdminDownloadDocumentAuditLogsRequestSchema)
.output(ZAdminDownloadDocumentAuditLogsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { envelopeId } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
const envelope = await prisma.envelope.findFirst({
where: unsafeBuildEnvelopeIdQuery(
{
type: 'envelopeId',
id: envelopeId,
},
EnvelopeType.DOCUMENT,
),
include: {
documentMeta: true,
envelopeItems: true,
user: {
select: {
id: true,
name: true,
email: true,
},
},
recipients: {
orderBy: {
id: 'asc',
},
},
fields: true,
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
const auditLogPdf = await generateAuditLogPdf({
envelope,
recipients: envelope.recipients,
fields: envelope.fields,
language: envelope.documentMeta.language,
envelopeOwner: {
email: envelope.user.email,
name: envelope.user.name || '',
},
envelopeItems: envelope.envelopeItems.map((item) => item.title),
pageWidth: PDF_SIZE_A4_72PPI.width,
pageHeight: PDF_SIZE_A4_72PPI.height,
});
const result = await auditLogPdf.save();
const base64 = Buffer.from(result).toString('base64');
return {
data: base64,
envelopeTitle: envelope.title,
};
});
@@ -0,0 +1,13 @@
import { z } from 'zod';
export const ZAdminDownloadDocumentAuditLogsRequestSchema = z.object({
envelopeId: z.string(),
});
export const ZAdminDownloadDocumentAuditLogsResponseSchema = z.object({
data: z.string(),
envelopeTitle: z.string(),
});
export type TAdminDownloadDocumentAuditLogsRequest = z.infer<typeof ZAdminDownloadDocumentAuditLogsRequestSchema>;
export type TAdminDownloadDocumentAuditLogsResponse = z.infer<typeof ZAdminDownloadDocumentAuditLogsResponseSchema>;
@@ -1,7 +1,6 @@
import { Prisma } from '@prisma/client';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { prisma } from '@documenso/prisma';
import { Prisma } from '@prisma/client';
import { adminProcedure } from '../trpc';
import {
@@ -120,14 +119,16 @@ export const findAdminOrganisations = async ({
};
}
const orderBy: Prisma.OrganisationOrderByWithRelationInput[] = query
? [{ subscription: { status: 'asc' } }, { name: 'asc' }]
: [{ createdAt: 'desc' }];
const [data, count] = await Promise.all([
prisma.organisation.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
createdAt: 'desc',
},
orderBy,
select: {
id: true,
createdAt: true,
@@ -1,9 +1,8 @@
import { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
import SubscriptionSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
import { z } from 'zod';
export const ZFindAdminOrganisationsRequestSchema = ZFindSearchParamsSchema.extend({
ownerUserId: z.number().optional(),
@@ -1,10 +1,9 @@
import { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { parseDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { unsafeBuildEnvelopeIdQuery } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { EnvelopeType } from '@prisma/client';
import { adminProcedure } from '../trpc';
import {
@@ -16,13 +15,7 @@ export const findDocumentAuditLogsRoute = adminProcedure
.input(ZFindDocumentAuditLogsRequestSchema)
.output(ZFindDocumentAuditLogsResponseSchema)
.query(async ({ input }) => {
const {
envelopeId,
page = 1,
perPage = 50,
orderByColumn = 'createdAt',
orderByDirection = 'desc',
} = input;
const { envelopeId, page = 1, perPage = 50, orderByColumn = 'createdAt', orderByDirection = 'desc' } = input;
const envelope = await prisma.envelope.findFirst({
where: unsafeBuildEnvelopeIdQuery(
@@ -1,7 +1,6 @@
import { z } from 'zod';
import { ZDocumentAuditLogSchema } from '@documenso/lib/types/document-audit-logs';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { z } from 'zod';
export const ZFindDocumentAuditLogsRequestSchema = ZFindSearchParamsSchema.extend({
envelopeId: z.string(),
@@ -1,18 +1,11 @@
import { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import {
mapSecondaryIdToDocumentId,
unsafeBuildEnvelopeIdQuery,
} from '@documenso/lib/utils/envelope';
import { mapSecondaryIdToDocumentId, unsafeBuildEnvelopeIdQuery } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { EnvelopeType } from '@prisma/client';
import { adminProcedure } from '../trpc';
import {
ZFindDocumentJobsRequestSchema,
ZFindDocumentJobsResponseSchema,
} from './find-document-jobs.types';
import { ZFindDocumentJobsRequestSchema, ZFindDocumentJobsResponseSchema } from './find-document-jobs.types';
export const findDocumentJobsRoute = adminProcedure
.input(ZFindDocumentJobsRequestSchema)
@@ -1,7 +1,6 @@
import { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import BackgroundJobSchema from '@documenso/prisma/generated/zod/modelSchema/BackgroundJobSchema';
import { z } from 'zod';
export const ZFindDocumentJobsRequestSchema = ZFindSearchParamsSchema.extend({
envelopeId: z.string(),
@@ -1,7 +1,6 @@
import { z } from 'zod';
import { ZDocumentManySchema } from '@documenso/lib/types/document';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { z } from 'zod';
export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
perPage: z.number().optional().default(20),
@@ -0,0 +1,92 @@
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { prisma } from '@documenso/prisma';
import { Prisma } from '@prisma/client';
import { adminProcedure } from '../trpc';
import { ZFindEmailDomainsRequestSchema, ZFindEmailDomainsResponseSchema } from './find-email-domains.types';
export const findEmailDomainsRoute = adminProcedure
.input(ZFindEmailDomainsRequestSchema)
.output(ZFindEmailDomainsResponseSchema)
.query(async ({ input }) => {
const { query, page, perPage, status } = input;
return await findEmailDomains({ query, page, perPage, status });
});
type FindEmailDomainsOptions = {
query?: string;
page?: number;
perPage?: number;
status?: 'PENDING' | 'ACTIVE';
};
const findEmailDomains = async ({ query, page = 1, perPage = 20, status }: FindEmailDomainsOptions) => {
const whereClause: Prisma.EmailDomainWhereInput = {};
if (query) {
whereClause.OR = [
{
domain: {
contains: query,
mode: Prisma.QueryMode.insensitive,
},
},
{
organisation: {
name: {
contains: query,
mode: Prisma.QueryMode.insensitive,
},
},
},
];
}
if (status) {
whereClause.status = status;
}
const [data, count] = await Promise.all([
prisma.emailDomain.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
domain: true,
status: true,
selector: true,
createdAt: true,
updatedAt: true,
lastVerifiedAt: true,
organisation: {
select: {
id: true,
name: true,
url: true,
},
},
_count: {
select: {
emails: true,
},
},
},
}),
prisma.emailDomain.count({
where: whereClause,
}),
]);
return {
data,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof data>;
};
@@ -0,0 +1,35 @@
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import EmailDomainStatusSchema from '@documenso/prisma/generated/zod/inputTypeSchemas/EmailDomainStatusSchema';
import EmailDomainSchema from '@documenso/prisma/generated/zod/modelSchema/EmailDomainSchema';
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
import { z } from 'zod';
export const ZFindEmailDomainsRequestSchema = ZFindSearchParamsSchema.extend({
status: EmailDomainStatusSchema.optional(),
});
export const ZFindEmailDomainsResponseSchema = ZFindResultResponse.extend({
data: EmailDomainSchema.pick({
id: true,
domain: true,
status: true,
selector: true,
createdAt: true,
updatedAt: true,
lastVerifiedAt: true,
})
.extend({
organisation: OrganisationSchema.pick({
id: true,
name: true,
url: true,
}),
_count: z.object({
emails: z.number(),
}),
})
.array(),
});
export type TFindEmailDomainsRequest = z.infer<typeof ZFindEmailDomainsRequestSchema>;
export type TFindEmailDomainsResponse = z.infer<typeof ZFindEmailDomainsResponseSchema>;
@@ -1,9 +1,8 @@
import type { Prisma } from '@prisma/client';
import type { z } from 'zod';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { prisma } from '@documenso/prisma';
import type SubscriptionClaimSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionClaimSchema';
import type { Prisma } from '@prisma/client';
import type { z } from 'zod';
import { adminProcedure } from '../trpc';
import {
@@ -26,11 +25,7 @@ type FindSubscriptionClaimsOptions = {
perPage?: number;
};
export const findSubscriptionClaims = async ({
query,
page = 1,
perPage = 50,
}: FindSubscriptionClaimsOptions) => {
export const findSubscriptionClaims = async ({ query, page = 1, perPage = 50 }: FindSubscriptionClaimsOptions) => {
let whereClause: Prisma.SubscriptionClaimWhereInput = {};
if (query) {
@@ -1,7 +1,6 @@
import type { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import SubscriptionClaimSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionClaimSchema';
import type { z } from 'zod';
export const ZFindSubscriptionClaimsRequestSchema = ZFindSearchParamsSchema.extend({});
@@ -0,0 +1,16 @@
import { adminFindUnsealedDocuments } from '@documenso/lib/server-only/admin/admin-find-unsealed-documents';
import { adminProcedure } from '../trpc';
import {
ZFindUnsealedDocumentsRequestSchema,
ZFindUnsealedDocumentsResponseSchema,
} from './find-unsealed-documents.types';
export const findUnsealedDocumentsRoute = adminProcedure
.input(ZFindUnsealedDocumentsRequestSchema)
.output(ZFindUnsealedDocumentsResponseSchema)
.query(async ({ input }) => {
const { page, perPage } = input;
return await adminFindUnsealedDocuments({ page, perPage });
});
@@ -0,0 +1,30 @@
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { z } from 'zod';
export const ZFindUnsealedDocumentsRequestSchema = ZFindSearchParamsSchema.pick({
page: true,
perPage: true,
}).extend({
perPage: z.number().optional().default(20),
});
export const ZAdminUnsealedDocumentSchema = z.object({
id: z.string(),
secondaryId: z.string(),
title: z.string(),
status: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
userId: z.number(),
teamId: z.number(),
ownerName: z.string().nullable(),
ownerEmail: z.string(),
lastSignedAt: z.date().nullable(),
});
export const ZFindUnsealedDocumentsResponseSchema = ZFindResultResponse.extend({
data: ZAdminUnsealedDocumentSchema.array(),
});
export type TFindUnsealedDocumentsRequest = z.infer<typeof ZFindUnsealedDocumentsRequestSchema>;
export type TFindUnsealedDocumentsResponse = z.infer<typeof ZFindUnsealedDocumentsResponseSchema>;
@@ -1,8 +1,7 @@
import { Prisma } from '@prisma/client';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { getHighestTeamRoleInGroup } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { Prisma } from '@prisma/client';
import { adminProcedure } from '../trpc';
import { ZFindUserTeamsRequestSchema, ZFindUserTeamsResponseSchema } from './find-user-teams.types';
@@ -1,9 +1,8 @@
import { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { TeamMemberRoleSchema } from '@documenso/prisma/generated/zod/inputTypeSchemas/TeamMemberRoleSchema';
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import { z } from 'zod';
export const ZFindUserTeamsRequestSchema = ZFindSearchParamsSchema.extend({
userId: z.number(),
@@ -1,5 +1,3 @@
import { z } from 'zod';
import { ZOrganisationSchema } from '@documenso/lib/types/organisation';
import OrganisationClaimSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationClaimSchema';
import OrganisationGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationGlobalSettingsSchema';
@@ -9,6 +7,7 @@ import OrganisationMemberSchema from '@documenso/prisma/generated/zod/modelSchem
import SubscriptionSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
import { z } from 'zod';
export const ZGetAdminOrganisationRequestSchema = z.object({
organisationId: z.string(),
@@ -0,0 +1,130 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisations';
import { getHighestTeamRoleInGroup } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { OrganisationMemberInviteStatus } from '@prisma/client';
import { adminProcedure } from '../trpc';
import { ZGetAdminTeamRequestSchema, ZGetAdminTeamResponseSchema } from './get-admin-team.types';
export const getAdminTeamRoute = adminProcedure
.input(ZGetAdminTeamRequestSchema)
.output(ZGetAdminTeamResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = input;
ctx.logger.info({
input: {
teamId,
},
});
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
include: {
organisation: {
select: {
id: true,
name: true,
url: true,
ownerUserId: true,
},
},
teamEmail: true,
teamGlobalSettings: true,
},
});
if (!team) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team not found',
});
}
const [teamMembers, pendingInvites] = await Promise.all([
prisma.organisationMember.findMany({
where: {
organisationId: team.organisationId,
organisationGroupMembers: {
some: {
group: {
teamGroups: {
some: {
teamId,
},
},
},
},
},
},
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
userId: true,
createdAt: true,
organisationGroupMembers: {
include: {
group: {
include: {
teamGroups: {
where: {
teamId,
},
},
},
},
},
},
user: {
select: {
id: true,
email: true,
name: true,
},
},
},
}),
// Invites are organisation-scoped in the schema (no team relation), so this is intentionally
// all pending invites for the team's parent organisation.
prisma.organisationMemberInvite.findMany({
where: {
organisationId: team.organisationId,
status: OrganisationMemberInviteStatus.PENDING,
},
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
email: true,
createdAt: true,
organisationRole: true,
status: true,
},
}),
]);
const mappedTeamMembers = teamMembers.map((teamMember) => {
const groups = teamMember.organisationGroupMembers.map(({ group }) => group);
return {
id: teamMember.id,
userId: teamMember.userId,
createdAt: teamMember.createdAt,
user: teamMember.user,
teamRole: getHighestTeamRoleInGroup(groups.flatMap((group) => group.teamGroups)),
organisationRole: getHighestOrganisationRoleInGroup(groups),
};
});
return {
...team,
memberCount: mappedTeamMembers.length,
teamMembers: mappedTeamMembers,
pendingInvites,
};
});
@@ -0,0 +1,51 @@
import { OrganisationMemberRoleSchema } from '@documenso/prisma/generated/zod/inputTypeSchemas/OrganisationMemberRoleSchema';
import { TeamMemberRoleSchema } from '@documenso/prisma/generated/zod/inputTypeSchemas/TeamMemberRoleSchema';
import OrganisationMemberInviteSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberInviteSchema';
import OrganisationMemberSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberSchema';
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
import TeamEmailSchema from '@documenso/prisma/generated/zod/modelSchema/TeamEmailSchema';
import TeamGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/TeamGlobalSettingsSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
import { z } from 'zod';
export const ZGetAdminTeamRequestSchema = z.object({
teamId: z.number().min(1),
});
export const ZGetAdminTeamResponseSchema = TeamSchema.extend({
organisation: OrganisationSchema.pick({
id: true,
name: true,
url: true,
ownerUserId: true,
}),
teamEmail: TeamEmailSchema.nullable(),
teamGlobalSettings: TeamGlobalSettingsSchema.nullable(),
memberCount: z.number(),
teamMembers: OrganisationMemberSchema.pick({
id: true,
userId: true,
createdAt: true,
})
.extend({
user: UserSchema.pick({
id: true,
email: true,
name: true,
}),
teamRole: TeamMemberRoleSchema,
organisationRole: OrganisationMemberRoleSchema,
})
.array(),
pendingInvites: OrganisationMemberInviteSchema.pick({
id: true,
email: true,
createdAt: true,
organisationRole: true,
status: true,
}).array(),
});
export type TGetAdminTeamRequest = z.infer<typeof ZGetAdminTeamRequestSchema>;
export type TGetAdminTeamResponse = z.infer<typeof ZGetAdminTeamResponseSchema>;
@@ -0,0 +1,39 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import { ZGetEmailDomainRequestSchema, ZGetEmailDomainResponseSchema } from './get-email-domain.types';
export const getEmailDomainRoute = adminProcedure
.input(ZGetEmailDomainRequestSchema)
.output(ZGetEmailDomainResponseSchema)
.query(async ({ input }) => {
const { emailDomainId } = input;
const emailDomain = await prisma.emailDomain.findUnique({
where: {
id: emailDomainId,
},
omit: {
privateKey: true,
},
include: {
organisation: {
select: {
id: true,
name: true,
url: true,
},
},
emails: true,
},
});
if (!emailDomain) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Email domain not found',
});
}
return emailDomain;
});
@@ -0,0 +1,29 @@
import { ZOrganisationEmailLiteSchema } from '@documenso/lib/types/organisation-email';
import EmailDomainSchema from '@documenso/prisma/generated/zod/modelSchema/EmailDomainSchema';
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
import { z } from 'zod';
export const ZGetEmailDomainRequestSchema = z.object({
emailDomainId: z.string(),
});
export const ZGetEmailDomainResponseSchema = EmailDomainSchema.pick({
id: true,
domain: true,
status: true,
selector: true,
publicKey: true,
createdAt: true,
updatedAt: true,
lastVerifiedAt: true,
}).extend({
organisation: OrganisationSchema.pick({
id: true,
name: true,
url: true,
}),
emails: ZOrganisationEmailLiteSchema.array(),
});
export type TGetEmailDomainRequest = z.infer<typeof ZGetEmailDomainRequestSchema>;
export type TGetEmailDomainResponse = z.infer<typeof ZGetEmailDomainResponseSchema>;
@@ -1,6 +1,5 @@
import { z } from 'zod';
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
import { z } from 'zod';
export const ZGetUserRequestSchema = z.object({
id: z.number().min(1),
@@ -1,9 +1,8 @@
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { generateDatabaseId } from '@documenso/lib/universal/id';
import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
import { adminProcedure } from '../trpc';
import {
@@ -78,13 +77,9 @@ export const promoteMemberToOwnerRoute = adminProcedure
);
// Find the current and target organisation groups
const currentMemberGroup = organisation.groups.find(
(group) => group.organisationRole === currentOrganisationRole,
);
const currentMemberGroup = organisation.groups.find((group) => group.organisationRole === currentOrganisationRole);
const adminGroup = organisation.groups.find(
(group) => group.organisationRole === OrganisationMemberRole.ADMIN,
);
const adminGroup = organisation.groups.find((group) => group.organisationRole === OrganisationMemberRole.ADMIN);
if (!currentMemberGroup) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
@@ -0,0 +1,22 @@
import { reregisterEmailDomain } from '@documenso/ee/server-only/lib/reregister-email-domain';
import { adminProcedure } from '../trpc';
import {
ZReregisterEmailDomainRequestSchema,
ZReregisterEmailDomainResponseSchema,
} from './reregister-email-domain.types';
export const reregisterEmailDomainRoute = adminProcedure
.input(ZReregisterEmailDomainRequestSchema)
.output(ZReregisterEmailDomainResponseSchema)
.mutation(async ({ input, ctx }) => {
const { emailDomainId } = input;
ctx.logger.info({
input: {
emailDomainId,
},
});
await reregisterEmailDomain({ emailDomainId });
});
@@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZReregisterEmailDomainRequestSchema = z.object({
emailDomainId: z.string(),
});
export const ZReregisterEmailDomainResponseSchema = z.void();
export type TReregisterEmailDomainRequest = z.infer<typeof ZReregisterEmailDomainRequestSchema>;
export type TReregisterEmailDomainResponse = z.infer<typeof ZReregisterEmailDomainResponseSchema>;
@@ -1,15 +1,11 @@
import { EnvelopeType } from '@prisma/client';
import { jobs } from '@documenso/lib/jobs/client';
import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { EnvelopeType } from '@prisma/client';
import { adminProcedure } from '../trpc';
import {
ZResealDocumentRequestSchema,
ZResealDocumentResponseSchema,
} from './reseal-document.types';
import { ZResealDocumentRequestSchema, ZResealDocumentResponseSchema } from './reseal-document.types';
export const resealDocumentRoute = adminProcedure
.input(ZResealDocumentRequestSchema)
@@ -2,10 +2,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZResetTwoFactorRequestSchema,
ZResetTwoFactorResponseSchema,
} from './reset-two-factor-authentication.types';
import { ZResetTwoFactorRequestSchema, ZResetTwoFactorResponseSchema } from './reset-two-factor-authentication.types';
export const resetTwoFactorRoute = adminProcedure
.input(ZResetTwoFactorRequestSchema)
@@ -3,22 +3,31 @@ import { createAdminOrganisationRoute } from './create-admin-organisation';
import { createStripeCustomerRoute } from './create-stripe-customer';
import { createSubscriptionClaimRoute } from './create-subscription-claim';
import { deleteDocumentRoute } from './delete-document';
import { deleteAdminOrganisationMemberRoute } from './delete-organisation-member';
import { deleteSubscriptionClaimRoute } from './delete-subscription-claim';
import { deleteAdminTeamMemberRoute } from './delete-team-member';
import { deleteUserRoute } from './delete-user';
import { disableUserRoute } from './disable-user';
import { downloadDocumentAuditLogsRoute } from './download-document-audit-logs';
import { enableUserRoute } from './enable-user';
import { findAdminOrganisationsRoute } from './find-admin-organisations';
import { findDocumentAuditLogsRoute } from './find-document-audit-logs';
import { findDocumentJobsRoute } from './find-document-jobs';
import { findDocumentsRoute } from './find-documents';
import { findEmailDomainsRoute } from './find-email-domains';
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
import { findUnsealedDocumentsRoute } from './find-unsealed-documents';
import { findUserTeamsRoute } from './find-user-teams';
import { getAdminOrganisationRoute } from './get-admin-organisation';
import { getAdminTeamRoute } from './get-admin-team';
import { getEmailDomainRoute } from './get-email-domain';
import { getUserRoute } from './get-user';
import { promoteMemberToOwnerRoute } from './promote-member-to-owner';
import { reregisterEmailDomainRoute } from './reregister-email-domain';
import { resealDocumentRoute } from './reseal-document';
import { resetTwoFactorRoute } from './reset-two-factor-authentication';
import { resyncLicenseRoute } from './resync-license';
import { swapOrganisationSubscriptionRoute } from './swap-organisation-subscription';
import { updateAdminOrganisationRoute } from './update-admin-organisation';
import { updateOrganisationMemberRoleRoute } from './update-organisation-member-role';
import { updateRecipientRoute } from './update-recipient';
@@ -32,10 +41,12 @@ export const adminRouter = router({
get: getAdminOrganisationRoute,
create: createAdminOrganisationRoute,
update: updateAdminOrganisationRoute,
swapSubscription: swapOrganisationSubscriptionRoute,
},
organisationMember: {
promoteToOwner: promoteMemberToOwnerRoute,
updateRole: updateOrganisationMemberRoleRoute,
delete: deleteAdminOrganisationMemberRoute,
},
claims: {
find: findSubscriptionClaimsRoute,
@@ -60,13 +71,26 @@ export const adminRouter = router({
},
document: {
find: findDocumentsRoute,
findUnsealed: findUnsealedDocumentsRoute,
delete: deleteDocumentRoute,
reseal: resealDocumentRoute,
findJobs: findDocumentJobsRoute,
findAuditLogs: findDocumentAuditLogsRoute,
downloadAuditLogs: downloadDocumentAuditLogsRoute,
},
recipient: {
update: updateRecipientRoute,
},
emailDomain: {
find: findEmailDomainsRoute,
get: getEmailDomainRoute,
reregister: reregisterEmailDomainRoute,
},
team: {
get: getAdminTeamRoute,
},
teamMember: {
delete: deleteAdminTeamMemberRoute,
},
updateSiteSetting: updateSiteSettingRoute,
});
@@ -0,0 +1,139 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createOrganisationClaimUpsertData } from '@documenso/lib/server-only/organisation/create-organisation';
import { INTERNAL_CLAIM_ID, internalClaims } from '@documenso/lib/types/subscription';
import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@prisma/client';
import { adminProcedure } from '../trpc';
import {
ZSwapOrganisationSubscriptionRequestSchema,
ZSwapOrganisationSubscriptionResponseSchema,
} from './swap-organisation-subscription.types';
export const swapOrganisationSubscriptionRoute = adminProcedure
.input(ZSwapOrganisationSubscriptionRequestSchema)
.output(ZSwapOrganisationSubscriptionResponseSchema)
.mutation(async ({ input, ctx }) => {
const { sourceOrganisationId, targetOrganisationId } = input;
ctx.logger.info({
input: {
sourceOrganisationId,
targetOrganisationId,
},
});
if (sourceOrganisationId === targetOrganisationId) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Source and target organisations must be different',
});
}
const sourceOrg = await prisma.organisation.findUnique({
where: { id: sourceOrganisationId },
include: {
subscription: true,
organisationClaim: true,
},
});
if (!sourceOrg) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Source organisation not found',
});
}
if (
!sourceOrg.subscription ||
(sourceOrg.subscription.status !== SubscriptionStatus.ACTIVE &&
sourceOrg.subscription.status !== SubscriptionStatus.PAST_DUE)
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Source organisation does not have an active subscription',
});
}
const targetOrg = await prisma.organisation.findUnique({
where: { id: targetOrganisationId },
include: {
subscription: true,
organisationClaim: true,
},
});
if (!targetOrg) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Target organisation not found',
});
}
if (sourceOrg.ownerUserId !== targetOrg.ownerUserId) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Both organisations must be owned by the same user',
});
}
if (
targetOrg.subscription &&
(targetOrg.subscription.status === SubscriptionStatus.ACTIVE ||
targetOrg.subscription.status === SubscriptionStatus.PAST_DUE)
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Target organisation already has an active subscription',
});
}
const customerId = sourceOrg.customerId ?? sourceOrg.subscription.customerId;
await prisma.$transaction(async (tx) => {
// Delete stale INACTIVE subscription on target if present.
if (targetOrg.subscription) {
await tx.subscription.delete({
where: { id: targetOrg.subscription.id },
});
}
// Clear customerId on source org to avoid unique constraint violation.
await tx.organisation.update({
where: { id: sourceOrganisationId },
data: { customerId: null },
});
// Set customerId on target org.
await tx.organisation.update({
where: { id: targetOrganisationId },
data: { customerId },
});
// Move the subscription record to the target org.
await tx.subscription.update({
where: { id: sourceOrg.subscription!.id },
data: { organisationId: targetOrganisationId },
});
// Copy source org's claim entitlements to target org's claim.
if (sourceOrg.organisationClaim && targetOrg.organisationClaim) {
await tx.organisationClaim.update({
where: { id: targetOrg.organisationClaim.id },
data: {
originalSubscriptionClaimId: sourceOrg.organisationClaim.originalSubscriptionClaimId,
teamCount: sourceOrg.organisationClaim.teamCount,
memberCount: sourceOrg.organisationClaim.memberCount,
envelopeItemCount: sourceOrg.organisationClaim.envelopeItemCount,
flags: sourceOrg.organisationClaim.flags,
},
});
}
// Reset source org's claim to FREE.
if (sourceOrg.organisationClaim) {
await tx.organisationClaim.update({
where: { id: sourceOrg.organisationClaim.id },
data: {
originalSubscriptionClaimId: INTERNAL_CLAIM_ID.FREE,
...createOrganisationClaimUpsertData(internalClaims[INTERNAL_CLAIM_ID.FREE]),
},
});
}
});
});
@@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZSwapOrganisationSubscriptionRequestSchema = z.object({
sourceOrganisationId: z.string(),
targetOrganisationId: z.string(),
});
export const ZSwapOrganisationSubscriptionResponseSchema = z.void();
export type TSwapOrganisationSubscriptionRequest = z.infer<typeof ZSwapOrganisationSubscriptionRequestSchema>;
export type TSwapOrganisationSubscriptionResponse = z.infer<typeof ZSwapOrganisationSubscriptionResponseSchema>;
@@ -1,9 +1,8 @@
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { generateDatabaseId } from '@documenso/lib/universal/id';
import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
import { adminProcedure } from '../trpc';
import {
@@ -88,9 +87,7 @@ export const updateOrganisationMemberRoleRoute = adminProcedure
(group) => group.organisationRole === currentOrganisationRole,
);
const adminGroup = organisation.groups.find(
(group) => group.organisationRole === OrganisationMemberRole.ADMIN,
);
const adminGroup = organisation.groups.find((group) => group.organisationRole === OrganisationMemberRole.ADMIN);
if (!currentMemberGroup) {
ctx.logger.error({
@@ -165,13 +162,9 @@ export const updateOrganisationMemberRoleRoute = adminProcedure
});
}
const currentMemberGroup = organisation.groups.find(
(group) => group.organisationRole === currentOrganisationRole,
);
const currentMemberGroup = organisation.groups.find((group) => group.organisationRole === currentOrganisationRole);
const newMemberGroup = organisation.groups.find(
(group) => group.organisationRole === targetRole,
);
const newMemberGroup = organisation.groups.find((group) => group.organisationRole === targetRole);
if (!currentMemberGroup) {
ctx.logger.error({
@@ -22,9 +22,5 @@ export const ZUpdateOrganisationMemberRoleRequestSchema = z.object({
export const ZUpdateOrganisationMemberRoleResponseSchema = z.void();
export type TUpdateOrganisationMemberRoleRequest = z.infer<
typeof ZUpdateOrganisationMemberRoleRequestSchema
>;
export type TUpdateOrganisationMemberRoleResponse = z.infer<
typeof ZUpdateOrganisationMemberRoleResponseSchema
>;
export type TUpdateOrganisationMemberRoleRequest = z.infer<typeof ZUpdateOrganisationMemberRoleRequestSchema>;
export type TUpdateOrganisationMemberRoleResponse = z.infer<typeof ZUpdateOrganisationMemberRoleResponseSchema>;
@@ -1,16 +1,13 @@
import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient';
import { adminProcedure } from '../trpc';
import {
ZUpdateRecipientRequestSchema,
ZUpdateRecipientResponseSchema,
} from './update-recipient.types';
import { ZUpdateRecipientRequestSchema, ZUpdateRecipientResponseSchema } from './update-recipient.types';
export const updateRecipientRoute = adminProcedure
.input(ZUpdateRecipientRequestSchema)
.output(ZUpdateRecipientResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id, name, email } = input;
const { id, name, email, role } = input;
ctx.logger.info({
input: {
@@ -18,5 +15,5 @@ export const updateRecipientRoute = adminProcedure
},
});
await updateRecipient({ id, name, email });
await updateRecipient({ id, name, email, role });
});
@@ -1,9 +1,11 @@
import { zEmail } from '@documenso/lib/utils/zod';
import { z } from 'zod';
export const ZUpdateRecipientRequestSchema = z.object({
id: z.number().min(1),
name: z.string().optional(),
email: z.string().email().optional(),
email: zEmail().optional(),
role: z.enum(['CC', 'SIGNER', 'VIEWER', 'APPROVER', 'ASSISTANT']).optional(),
});
export const ZUpdateRecipientResponseSchema = z.void();
@@ -1,10 +1,7 @@
import { upsertSiteSetting } from '@documenso/lib/server-only/site-settings/upsert-site-setting';
import { adminProcedure } from '../trpc';
import {
ZUpdateSiteSettingRequestSchema,
ZUpdateSiteSettingResponseSchema,
} from './update-site-setting.types';
import { ZUpdateSiteSettingRequestSchema, ZUpdateSiteSettingResponseSchema } from './update-site-setting.types';
export const updateSiteSettingRoute = adminProcedure
.input(ZUpdateSiteSettingRequestSchema)
@@ -1,6 +1,5 @@
import { z } from 'zod';
import { ZSiteSettingSchema } from '@documenso/lib/server-only/site-settings/schema';
import { z } from 'zod';
export const ZUpdateSiteSettingRequestSchema = ZSiteSettingSchema;
@@ -29,30 +29,25 @@ export const updateSubscriptionClaimRoute = adminProcedure
const newlyEnabledFlags = getNewTruthyFlags(existingClaim.flags, data.flags);
await prisma.$transaction(async (tx) => {
await tx.subscriptionClaim.update({
where: {
id,
},
data,
});
if (Object.keys(newlyEnabledFlags).length > 0) {
await jobsClient.triggerJob({
name: 'internal.backport-subscription-claims',
payload: {
subscriptionClaimId: id,
flags: newlyEnabledFlags,
},
});
}
await prisma.subscriptionClaim.update({
where: {
id,
},
data,
});
if (Object.keys(newlyEnabledFlags).length > 0) {
await jobsClient.triggerJob({
name: 'internal.backport-subscription-claims',
payload: {
subscriptionClaimId: id,
flags: newlyEnabledFlags,
},
});
}
});
function getNewTruthyFlags(
a: Partial<TClaimFlags>,
b: Partial<TClaimFlags>,
): Record<keyof TClaimFlags, true> {
function getNewTruthyFlags(a: Partial<TClaimFlags>, b: Partial<TClaimFlags>): Record<keyof TClaimFlags, true> {
const flags: { [key in keyof TClaimFlags]?: true } = {};
for (const key in b) {
@@ -1,10 +1,11 @@
import { zEmail } from '@documenso/lib/utils/zod';
import { Role } from '@prisma/client';
import { z } from 'zod';
export const ZUpdateUserRequestSchema = z.object({
id: z.number().min(1),
name: z.string().nullish(),
email: z.string().email().optional(),
email: zEmail().optional(),
roles: z.array(z.nativeEnum(Role)).optional(),
});