Files
zooqueen 15d742477b fix: create documents and templates on a fresh database
Envelope id allocation now establishes the counter row it reads, so the
first document on a database that has never allocated one gets id 1.

Nothing inside a transaction reaches the global Prisma client any more.
Base SQLite serves one write connection, so a second client waiting on
the lock the transaction already holds stalls until the busy timeout and
the transaction dies. Webhooks, job triggers and verification emails are
side effects of committed state, so they fire after the commit; the
organisation link no longer nests a second transaction inside itself; and
a recipient lookup in the v1 field API reads on its own transaction.

EXISTS subqueries name a column. Postgres accepts an empty select list,
SQLite does not, which is what stopped the seal sweep, the unsealed
document view and signing volume from running at all.

Also:
- the profile form saves a name before a signature exists, matching the
  server schema, instead of failing validation with nothing to show
- every upload reads its error from one place, so a failed template
  upload says template
- the documents filter strip scrolls its selected tab into view
- the inbox and support headings use the shared page header
- one settings nav item, so a click lands on the link and fires once
- the command dialog names itself for screen readers, and the mobile
  search and menu buttons carry labels
- documentation links point at docs.hanzo.ai
- the organisations row links to the organisation
- the app is called Hanzo Sign throughout, the wordmark variables are
  declared to turbo, and the webhook secret rides a valid header name
2026-08-14 01:15:39 -07:00

1276 lines
36 KiB
TypeScript

// esign ZAP handlers — admin router (30/30 procedures).
//
// Verbatim tRPC procedure bodies rewired to (ctx, raw). Input is validated by
// the SAME Zod schema the tRPC procedure used; the server-only functions and
// the per-route exported helpers (getAdminOrganisation / findAdminOrganisations
// / findSubscriptionClaims / resetTwoFactor) are reused unchanged. The two
// non-exported tRPC helpers (findUserTeams / findEmailDomains) have their bodies
// ported inline. Route keys mirror the tRPC nested router shape (see
// admin-router.zap).
//
// SECURITY — admin re-assertion: tRPC's `adminProcedure` runs `adminMiddleware`
// (server/trpc.ts), which throws UNAUTHORIZED unless `isAdmin(ctx.user)`. The
// ZAP MintCap (mint.ts) only establishes user identity; it does NOT carry the
// `roles` needed for `isAdmin`. So every handler below calls `assertAdmin(ctx)`
// first, which loads the caller's roles via getUserById and replicates the
// exact `isAdmin` check — re-asserting the admin boundary the middleware enforced.
import type { Prisma } from '@prisma/client';
import {
EnvelopeType,
OrganisationGroupType,
OrganisationMemberRole,
OrganisationType,
SubscriptionStatus,
} from '@prisma/client';
import { AppError, AppErrorCode } from '@hanzo/esign-lib/errors/app-error';
import { jobs, jobsClient } from '@hanzo/esign-lib/jobs/client';
import { adminFindDocuments } from '@hanzo/esign-lib/server-only/admin/admin-find-documents';
import { adminFindUnsealedDocuments } from '@hanzo/esign-lib/server-only/admin/admin-find-unsealed-documents';
import { adminSuperDeleteDocument } from '@hanzo/esign-lib/server-only/admin/admin-super-delete-document';
import { unsafeGetEntireEnvelope } from '@hanzo/esign-lib/server-only/admin/get-entire-document';
import { updateRecipient } from '@hanzo/esign-lib/server-only/admin/update-recipient';
import { updateUser } from '@hanzo/esign-lib/server-only/admin/update-user';
import { sendDeleteEmail } from '@hanzo/esign-lib/server-only/document/send-delete-email';
import { reregisterEmailDomain } from '@hanzo/esign-lib/server-only/email-domain/reregister-email-domain';
import { LicenseClient } from '@hanzo/esign-lib/server-only/license/license-client';
import {
createOrganisation,
createOrganisationClaimUpsertData,
} from '@hanzo/esign-lib/server-only/organisation/create-organisation';
import { upsertSiteSetting } from '@hanzo/esign-lib/server-only/site-settings/upsert-site-setting';
import { deleteUser } from '@hanzo/esign-lib/server-only/user/delete-user';
import { disableUser } from '@hanzo/esign-lib/server-only/user/disable-user';
import { enableUser } from '@hanzo/esign-lib/server-only/user/enable-user';
import { getUserById } from '@hanzo/esign-lib/server-only/user/get-user-by-id';
import type { FindResultResponse } from '@hanzo/esign-lib/types/search-params';
import { INTERNAL_CLAIM_ID, internalClaims } from '@hanzo/esign-lib/types/subscription';
import type { TClaimFlags } from '@hanzo/esign-lib/types/subscription';
import { generateDatabaseId } from '@hanzo/esign-lib/universal/id';
import { isDocumentCompleted, mapEnvelopesToDocumentMany } from '@hanzo/esign-lib/utils/document';
import { parseDocumentAuditLogData } from '@hanzo/esign-lib/utils/document-audit-logs';
import {
mapSecondaryIdToDocumentId,
unsafeBuildEnvelopeIdQuery,
} from '@hanzo/esign-lib/utils/envelope';
import { isAdmin } from '@hanzo/esign-lib/utils/is-admin';
import { getHighestOrganisationRoleInGroup } from '@hanzo/esign-lib/utils/organisations';
import { getHighestTeamRoleInGroup } from '@hanzo/esign-lib/utils/teams';
import { prisma } from '@hanzo/esign-prisma';
import { ZCreateAdminOrganisationRequestSchema } from '../../../server/admin-router/create-admin-organisation.types';
import { ZCreateSubscriptionClaimRequestSchema } from '../../../server/admin-router/create-subscription-claim.types';
import { ZDeleteDocumentRequestSchema } from '../../../server/admin-router/delete-document.types';
import { ZDeleteSubscriptionClaimRequestSchema } from '../../../server/admin-router/delete-subscription-claim.types';
import { ZDeleteUserRequestSchema } from '../../../server/admin-router/delete-user.types';
import { ZDisableUserRequestSchema } from '../../../server/admin-router/disable-user.types';
import { ZEnableUserRequestSchema } from '../../../server/admin-router/enable-user.types';
import { findAdminOrganisations } from '../../../server/admin-router/find-admin-organisations';
import { ZFindAdminOrganisationsRequestSchema } from '../../../server/admin-router/find-admin-organisations.types';
import { ZFindDocumentAuditLogsRequestSchema } from '../../../server/admin-router/find-document-audit-logs.types';
import { ZFindDocumentJobsRequestSchema } from '../../../server/admin-router/find-document-jobs.types';
import { ZFindDocumentsRequestSchema } from '../../../server/admin-router/find-documents.types';
import { ZFindEmailDomainsRequestSchema } from '../../../server/admin-router/find-email-domains.types';
import { findSubscriptionClaims } from '../../../server/admin-router/find-subscription-claims';
import { ZFindSubscriptionClaimsRequestSchema } from '../../../server/admin-router/find-subscription-claims.types';
import { ZFindUnsealedDocumentsRequestSchema } from '../../../server/admin-router/find-unsealed-documents.types';
import { ZFindUserTeamsRequestSchema } from '../../../server/admin-router/find-user-teams.types';
import { getAdminOrganisation } from '../../../server/admin-router/get-admin-organisation';
import { ZGetAdminOrganisationRequestSchema } from '../../../server/admin-router/get-admin-organisation.types';
import { ZGetEmailDomainRequestSchema } from '../../../server/admin-router/get-email-domain.types';
import { ZGetUserRequestSchema } from '../../../server/admin-router/get-user.types';
import { ZPromoteMemberToOwnerRequestSchema } from '../../../server/admin-router/promote-member-to-owner.types';
import { ZReregisterEmailDomainRequestSchema } from '../../../server/admin-router/reregister-email-domain.types';
import { ZResealDocumentRequestSchema } from '../../../server/admin-router/reseal-document.types';
import { resetTwoFactor } from '../../../server/admin-router/reset-two-factor-authentication';
import { ZResetTwoFactorRequestSchema } from '../../../server/admin-router/reset-two-factor-authentication.types';
import { ZResyncLicenseRequestSchema } from '../../../server/admin-router/resync-license.types';
import { ZSwapOrganisationSubscriptionRequestSchema } from '../../../server/admin-router/swap-organisation-subscription.types';
import { ZUpdateAdminOrganisationRequestSchema } from '../../../server/admin-router/update-admin-organisation.types';
import { ZUpdateOrganisationMemberRoleRequestSchema } from '../../../server/admin-router/update-organisation-member-role.types';
import { ZUpdateRecipientRequestSchema } from '../../../server/admin-router/update-recipient.types';
import { ZUpdateSiteSettingRequestSchema } from '../../../server/admin-router/update-site-setting.types';
import { ZUpdateSubscriptionClaimRequestSchema } from '../../../server/admin-router/update-subscription-claim.types';
import { ZUpdateUserRequestSchema } from '../../../server/admin-router/update-user.types';
import type { ZapContext } from '../context';
import type { ZapRouteMap } from '../dispatch';
/**
* Re-assert the admin boundary that tRPC's `adminMiddleware` enforced.
*
* The MintCap only proves user identity (id/name/email) — it does NOT carry
* `roles`. We load the caller's roles by id and apply the SAME `isAdmin` check
* the middleware used. A non-admin caller is rejected with UNAUTHORIZED, exactly
* like `adminProcedure` would have.
*/
async function assertAdmin(ctx: ZapContext): Promise<void> {
const user = await getUserById({ id: ctx.user.id }).catch(() => null);
if (!user || !isAdmin(user)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Not authorized to perform this action.',
});
}
}
function getNewTruthyFlags(
a: Partial<TClaimFlags>,
b: Partial<TClaimFlags>,
): Record<keyof TClaimFlags, true> {
const flags: { [key in keyof TClaimFlags]?: true } = {};
for (const key in b) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const typedKey = key as keyof TClaimFlags;
if (b[typedKey] === true && a[typedKey] !== true) {
flags[typedKey] = true;
}
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return flags as Record<keyof TClaimFlags, true>;
}
export const adminRoutes: ZapRouteMap = {
'admin.organisation.find': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { query, page, perPage, ownerUserId, memberUserId } =
ZFindAdminOrganisationsRequestSchema.parse(raw);
return await findAdminOrganisations({
query,
page,
perPage,
ownerUserId,
memberUserId,
});
},
'admin.organisation.get': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { organisationId } = ZGetAdminOrganisationRequestSchema.parse(raw);
ctx.logger.info({
input: {
organisationId,
},
});
return await getAdminOrganisation({
organisationId,
});
},
'admin.organisation.create': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { ownerUserId, data } = ZCreateAdminOrganisationRequestSchema.parse(raw);
ctx.logger.info({
input: {
ownerUserId,
},
});
const organisation = await createOrganisation({
userId: ownerUserId,
name: data.name,
type: OrganisationType.ORGANISATION,
claim: internalClaims.free,
});
return {
organisationId: organisation.id,
};
},
'admin.organisation.update': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { organisationId, data } = ZUpdateAdminOrganisationRequestSchema.parse(raw);
ctx.logger.info({
input: {
organisationId,
},
});
const organisation = await prisma.organisation.findUnique({
where: {
id: organisationId,
},
include: {
organisationClaim: true,
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const { name, url, customerId, claims, originalSubscriptionClaimId } = data;
await prisma.organisation.update({
where: {
id: organisationId,
},
data: {
name,
url,
customerId: customerId ? customerId : undefined,
},
});
await prisma.organisationClaim.update({
where: {
id: organisation.organisationClaimId,
},
data: {
...claims,
originalSubscriptionClaimId,
},
});
},
'admin.organisation.swapSubscription': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { sourceOrganisationId, targetOrganisationId } =
ZSwapOrganisationSubscriptionRequestSchema.parse(raw);
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]),
},
});
}
});
},
'admin.organisationMember.promoteToOwner': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { organisationId, userId } = ZPromoteMemberToOwnerRequestSchema.parse(raw);
ctx.logger.info({
input: {
organisationId,
userId,
},
});
// First, verify the organisation exists and get member details with groups
const organisation = await prisma.organisation.findUnique({
where: {
id: organisationId,
},
include: {
groups: {
where: {
type: OrganisationGroupType.INTERNAL_ORGANISATION,
},
},
members: {
where: {
userId,
},
include: {
organisationGroupMembers: {
include: {
group: true,
},
},
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation not found',
});
}
// Verify the user is a member of the organisation
const [member] = organisation.members;
if (!member) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User is not a member of this organisation',
});
}
// Verify the user is not already the owner
if (organisation.ownerUserId === userId) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'User is already the owner of this organisation',
});
}
// Get current organisation role
const currentOrganisationRole = getHighestOrganisationRoleInGroup(
member.organisationGroupMembers.flatMap((member) => member.group),
);
// Find the current and target organisation groups
const currentMemberGroup = organisation.groups.find(
(group) => group.organisationRole === currentOrganisationRole,
);
const adminGroup = organisation.groups.find(
(group) => group.organisationRole === OrganisationMemberRole.ADMIN,
);
if (!currentMemberGroup) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Current member group not found',
});
}
if (!adminGroup) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Admin group not found',
});
}
// Update the organisation owner and member role in a transaction
await prisma.$transaction(async (tx) => {
// Update the organisation to set the new owner
await tx.organisation.update({
where: {
id: organisationId,
},
data: {
ownerUserId: userId,
},
});
// Only update role if the user is not already an admin then add them to the admin group
if (currentOrganisationRole !== OrganisationMemberRole.ADMIN) {
await tx.organisationGroupMember.create({
data: {
id: generateDatabaseId('group_member'),
organisationMemberId: member.id,
groupId: adminGroup.id,
},
});
}
});
},
'admin.organisationMember.updateRole': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { organisationId, userId, role } = ZUpdateOrganisationMemberRoleRequestSchema.parse(raw);
ctx.logger.info({
input: {
organisationId,
userId,
role,
},
});
const organisation = await prisma.organisation.findUnique({
where: {
id: organisationId,
},
include: {
groups: {
where: {
type: OrganisationGroupType.INTERNAL_ORGANISATION,
},
},
members: {
where: {
userId,
},
include: {
organisationGroupMembers: {
include: {
group: true,
},
},
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation not found',
});
}
const [member] = organisation.members;
if (!member) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User is not a member of this organisation',
});
}
const currentOrganisationRole = getHighestOrganisationRoleInGroup(
member.organisationGroupMembers.flatMap((member) => member.group),
);
if (role === 'OWNER') {
if (organisation.ownerUserId === userId) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'User is already the owner of this organisation',
});
}
const currentMemberGroup = organisation.groups.find(
(group) => group.organisationRole === currentOrganisationRole,
);
const adminGroup = organisation.groups.find(
(group) => group.organisationRole === OrganisationMemberRole.ADMIN,
);
if (!currentMemberGroup) {
ctx.logger.error({
message: '[CRITICAL]: Missing internal group',
organisationId,
userId,
role: currentOrganisationRole,
});
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Current member group not found',
});
}
if (!adminGroup) {
ctx.logger.error({
message: '[CRITICAL]: Missing internal group',
organisationId,
userId,
targetRole: 'ADMIN',
});
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Admin group not found',
});
}
await prisma.$transaction(async (tx) => {
await tx.organisation.update({
where: {
id: organisationId,
},
data: {
ownerUserId: userId,
},
});
if (currentOrganisationRole !== OrganisationMemberRole.ADMIN) {
await tx.organisationGroupMember.delete({
where: {
organisationMemberId_groupId: {
organisationMemberId: member.id,
groupId: currentMemberGroup.id,
},
},
});
await tx.organisationGroupMember.create({
data: {
id: generateDatabaseId('group_member'),
organisationMemberId: member.id,
groupId: adminGroup.id,
},
});
}
});
return;
}
const targetRole = role as OrganisationMemberRole;
if (currentOrganisationRole === targetRole) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'User already has this role',
});
}
if (userId === organisation.ownerUserId && targetRole !== OrganisationMemberRole.ADMIN) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Organisation owner must be an admin. Transfer ownership first.',
});
}
const currentMemberGroup = organisation.groups.find(
(group) => group.organisationRole === currentOrganisationRole,
);
const newMemberGroup = organisation.groups.find(
(group) => group.organisationRole === targetRole,
);
if (!currentMemberGroup) {
ctx.logger.error({
message: '[CRITICAL]: Missing internal group',
organisationId,
userId,
role: currentOrganisationRole,
});
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Current member group not found',
});
}
if (!newMemberGroup) {
ctx.logger.error({
message: '[CRITICAL]: Missing internal group',
organisationId,
userId,
targetRole,
});
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'New member group not found',
});
}
await prisma.$transaction(async (tx) => {
await tx.organisationGroupMember.delete({
where: {
organisationMemberId_groupId: {
organisationMemberId: member.id,
groupId: currentMemberGroup.id,
},
},
});
await tx.organisationGroupMember.create({
data: {
id: generateDatabaseId('group_member'),
organisationMemberId: member.id,
groupId: newMemberGroup.id,
},
});
});
},
'admin.claims.find': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { query, page, perPage } = ZFindSubscriptionClaimsRequestSchema.parse(raw);
return await findSubscriptionClaims({ query, page, perPage });
},
'admin.claims.create': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const input = ZCreateSubscriptionClaimRequestSchema.parse(raw);
const { name, teamCount, memberCount, envelopeItemCount, flags } = input;
ctx.logger.info({
input,
});
await prisma.subscriptionClaim.create({
data: {
name,
teamCount,
envelopeItemCount,
memberCount,
flags,
},
});
},
'admin.claims.update': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const input = ZUpdateSubscriptionClaimRequestSchema.parse(raw);
const { id, data } = input;
ctx.logger.info({
input,
});
const existingClaim = await prisma.subscriptionClaim.findUnique({
where: { id },
});
if (!existingClaim) {
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Subscription claim not found' });
}
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,
},
});
}
},
'admin.claims.delete': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { id } = ZDeleteSubscriptionClaimRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
},
});
const existingClaim = await prisma.subscriptionClaim.findFirst({
where: {
id,
},
});
if (!existingClaim) {
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Subscription claim not found' });
}
if (existingClaim.locked) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Cannot delete locked subscription claim',
});
}
await prisma.subscriptionClaim.delete({
where: {
id,
},
});
},
'admin.license.resync': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
ZResyncLicenseRequestSchema.parse(raw);
const client = LicenseClient.getInstance();
if (!client) {
return;
}
await client.resync();
},
'admin.user.get': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { id } = ZGetUserRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
},
});
return await getUserById({ id });
},
'admin.user.update': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { id, name, email, roles } = ZUpdateUserRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
roles,
},
});
await updateUser({ id, name, email, roles });
},
'admin.user.delete': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { id } = ZDeleteUserRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
},
});
await deleteUser({ id });
},
'admin.user.enable': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { id } = ZEnableUserRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
},
});
const user = await getUserById({ id }).catch(() => null);
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
await enableUser({ id });
},
'admin.user.disable': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { id } = ZDisableUserRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
},
});
const user = await getUserById({ id }).catch(() => null);
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
await disableUser({ id });
},
'admin.user.resetTwoFactor': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { userId } = ZResetTwoFactorRequestSchema.parse(raw);
ctx.logger.info({
input: {
userId,
},
});
return await resetTwoFactor({ userId });
},
'admin.user.findTeams': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { userId, query, page = 1, perPage = 10 } = ZFindUserTeamsRequestSchema.parse(raw);
const whereClause: Prisma.TeamWhereInput = {
teamGroups: {
some: {
organisationGroup: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
},
};
if (query && query.length > 0) {
whereClause.name = {
contains: query,
};
}
const [data, count] = await Promise.all([
prisma.team.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
createdAt: 'desc',
},
include: {
organisation: {
select: {
id: true,
name: true,
url: true,
},
},
teamGroups: {
where: {
organisationGroup: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
},
},
}),
prisma.team.count({
where: whereClause,
}),
]);
const mappedData = data.map((team) => ({
id: team.id,
name: team.name,
url: team.url,
createdAt: team.createdAt,
teamRole: getHighestTeamRoleInGroup(team.teamGroups),
organisation: team.organisation,
}));
return {
data: mappedData,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof mappedData>;
},
'admin.document.find': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { query, page, perPage } = ZFindDocumentsRequestSchema.parse(raw);
const result = await adminFindDocuments({ query, page, perPage });
return {
...result,
data: result.data.map(mapEnvelopesToDocumentMany),
};
},
'admin.document.findUnsealed': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { page, perPage } = ZFindUnsealedDocumentsRequestSchema.parse(raw);
return await adminFindUnsealedDocuments({ page, perPage });
},
'admin.document.delete': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { id, reason } = ZDeleteDocumentRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
},
});
await sendDeleteEmail({ envelopeId: id, reason });
await adminSuperDeleteDocument({
envelopeId: id,
requestMetadata: ctx.metadata.requestMetadata,
});
},
'admin.document.reseal': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { id } = ZResealDocumentRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
},
});
const envelope = await unsafeGetEntireEnvelope({
id: {
type: 'envelopeId',
id,
},
type: EnvelopeType.DOCUMENT,
});
const isResealing = isDocumentCompleted(envelope.status);
await jobs.triggerJob({
name: 'internal.seal-document',
payload: {
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
isResealing,
},
});
},
'admin.document.findJobs': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { envelopeId, page = 1, perPage = 5 } = ZFindDocumentJobsRequestSchema.parse(raw);
const envelope = await prisma.envelope.findFirst({
where: unsafeBuildEnvelopeIdQuery(
{
type: 'envelopeId',
id: envelopeId,
},
EnvelopeType.DOCUMENT,
),
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
const [data, count] = await Promise.all([
prisma.backgroundJob.findMany({
where: {
jobId: 'internal.seal-document',
payload: {
// SQLite Json filter uses a JSONPath string (`$.documentId`),
// not Postgres' array form (`['documentId']`).
path: '$.documentId',
equals: mapSecondaryIdToDocumentId(envelope.secondaryId),
},
},
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
submittedAt: 'desc',
},
}),
prisma.backgroundJob.count({
where: {
jobId: 'internal.seal-document',
payload: {
// SQLite Json filter uses a JSONPath string (`$.documentId`),
// not Postgres' array form (`['documentId']`).
path: '$.documentId',
equals: mapSecondaryIdToDocumentId(envelope.secondaryId),
},
},
}),
]);
return {
data,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof data>;
},
'admin.document.findAuditLogs': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const {
envelopeId,
page = 1,
perPage = 50,
orderByColumn = 'createdAt',
orderByDirection = 'desc',
} = ZFindDocumentAuditLogsRequestSchema.parse(raw);
const envelope = await prisma.envelope.findFirst({
where: unsafeBuildEnvelopeIdQuery(
{
type: 'envelopeId',
id: envelopeId,
},
EnvelopeType.DOCUMENT,
),
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
const [data, count] = await Promise.all([
prisma.documentAuditLog.findMany({
where: { envelopeId: envelope.id },
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
[orderByColumn]: orderByDirection,
},
}),
prisma.documentAuditLog.count({
where: { envelopeId: envelope.id },
}),
]);
const parsedData = data.map((auditLog) => parseDocumentAuditLogData(auditLog));
return {
data: parsedData,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof parsedData>;
},
'admin.recipient.update': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { id, name, email } = ZUpdateRecipientRequestSchema.parse(raw);
ctx.logger.info({
input: {
id,
},
});
await updateRecipient({ id, name, email });
},
'admin.emailDomain.find': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { query, page = 1, perPage = 20, status } = ZFindEmailDomainsRequestSchema.parse(raw);
const whereClause: Prisma.EmailDomainWhereInput = {};
if (query) {
whereClause.OR = [
{
domain: {
contains: query,
},
},
{
organisation: {
name: {
contains: query,
},
},
},
];
}
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>;
},
'admin.emailDomain.get': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { emailDomainId } = ZGetEmailDomainRequestSchema.parse(raw);
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;
},
'admin.emailDomain.reregister': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { emailDomainId } = ZReregisterEmailDomainRequestSchema.parse(raw);
ctx.logger.info({
input: {
emailDomainId,
},
});
await reregisterEmailDomain({ emailDomainId });
},
'admin.updateSiteSetting': async (ctx: ZapContext, raw) => {
await assertAdmin(ctx);
const { ...siteSetting } = ZUpdateSiteSettingRequestSchema.parse(raw);
ctx.logger.info({
input: {
id: siteSetting.id,
},
});
await upsertSiteSetting({
...siteSetting,
userId: ctx.user.id,
});
},
};