This commit is contained in:
David Nguyen
2025-05-07 15:03:20 +10:00
parent 419bc02171
commit 7abfc9e271
390 changed files with 21254 additions and 12607 deletions

View File

@ -19,11 +19,9 @@ import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
import { getStats } from '@documenso/lib/server-only/document/get-stats';
import { moveDocumentToTeam } from '@documenso/lib/server-only/document/move-document-to-team';
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
@ -50,18 +48,12 @@ import {
ZGetDocumentByTokenQuerySchema,
ZGetDocumentWithDetailsByIdRequestSchema,
ZGetDocumentWithDetailsByIdResponseSchema,
ZMoveDocumentToTeamResponseSchema,
ZMoveDocumentToTeamSchema,
ZResendDocumentMutationSchema,
ZSearchDocumentsMutationSchema,
ZSetSigningOrderForDocumentMutationSchema,
ZSuccessResponseSchema,
} from './schema';
import { updateDocumentRoute } from './update-document';
import {
ZUpdateDocumentRequestSchema,
ZUpdateDocumentResponseSchema,
} from './update-document.types';
export const documentRouter = router({
/**
@ -167,7 +159,7 @@ export const documentRouter = router({
teamId: team.id,
teamEmail: team.teamEmail?.email,
senderIds,
currentTeamMemberRole: team.currentTeamMember?.role,
currentTeamMemberRole: team.currentTeamRole,
currentUserEmail: user.email,
userId: user.id,
};
@ -340,49 +332,6 @@ export const documentRouter = router({
updateDocument: updateDocumentRoute,
/**
* @deprecated Delete this after updateDocument endpoint is deployed
*/
setSettingsForDocument: authenticatedProcedure
.input(ZUpdateDocumentRequestSchema)
.output(ZUpdateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, data, meta = {} } = input;
const userId = ctx.user.id;
if (Object.values(meta).length > 0) {
await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
subject: meta.subject,
message: meta.message,
timezone: meta.timezone,
dateFormat: meta.dateFormat,
language: meta.language,
typedSignatureEnabled: meta.typedSignatureEnabled,
uploadSignatureEnabled: meta.uploadSignatureEnabled,
drawSignatureEnabled: meta.drawSignatureEnabled,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
signingOrder: meta.signingOrder,
allowDictateNextSigner: meta.allowDictateNextSigner,
emailSettings: meta.emailSettings,
requestMetadata: ctx.metadata,
});
}
return await updateDocument({
userId,
teamId,
documentId,
data,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
@ -413,33 +362,6 @@ export const documentRouter = router({
return ZGenericSuccessResponse;
}),
/**
* @public
*/
moveDocumentToTeam: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/move',
summary: 'Move document',
description: 'Move a document from your personal account to a team',
tags: ['Document'],
},
})
.input(ZMoveDocumentToTeamSchema)
.output(ZMoveDocumentToTeamResponseSchema)
.mutation(async ({ input, ctx }) => {
const { documentId, teamId } = input;
const userId = ctx.user.id;
return await moveDocumentToTeam({
documentId,
teamId,
userId,
requestMetadata: ctx.metadata,
});
}),
/**
* @private
*

View File

@ -340,10 +340,3 @@ export const ZDownloadAuditLogsMutationSchema = z.object({
export const ZDownloadCertificateMutationSchema = z.object({
documentId: z.number(),
});
export const ZMoveDocumentToTeamSchema = z.object({
documentId: z.number().describe('The ID of the document to move to a team.'),
teamId: z.number().describe('The ID of the team to move the document to.'),
});
export const ZMoveDocumentToTeamResponseSchema = ZDocumentLiteSchema;

View File

@ -0,0 +1,18 @@
import { acceptOrganisationInvitation } from '@documenso/lib/server-only/organisation/accept-organisation-invitation';
import { maybeAuthenticatedProcedure } from '../trpc';
import {
ZAcceptOrganisationMemberInviteRequestSchema,
ZAcceptOrganisationMemberInviteResponseSchema,
} from './accept-organisation-member-invite.types';
export const acceptOrganisationMemberInviteRoute = maybeAuthenticatedProcedure
.input(ZAcceptOrganisationMemberInviteRequestSchema)
.output(ZAcceptOrganisationMemberInviteResponseSchema)
.mutation(async ({ input }) => {
const { token } = input;
return await acceptOrganisationInvitation({
token,
});
});

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZAcceptOrganisationMemberInviteRequestSchema = z.object({
token: z.string(),
});
export const ZAcceptOrganisationMemberInviteResponseSchema = z.void();
export type TAcceptOrganisationMemberInviteResponse = z.infer<
typeof ZAcceptOrganisationMemberInviteResponseSchema
>;

View File

@ -0,0 +1,73 @@
import { OrganisationGroupType } from '@prisma/client';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateOrganisationGroupRequestSchema,
ZCreateOrganisationGroupResponseSchema,
} from './create-organisation-group.types';
export const createOrganisationGroupRoute = authenticatedProcedure
// .meta(createOrganisationGroupMeta)
.input(ZCreateOrganisationGroupRequestSchema)
.output(ZCreateOrganisationGroupResponseSchema)
.mutation(async ({ input, ctx }) => {
const { organisationId, organisationRole, name, memberIds } = input;
const { user } = ctx;
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
user.id,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
include: {
groups: true,
members: {
include: {
user: true,
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
// const currentUserOrganisationRole = getHighestOrganisationRoleInGroup()
// Todo: orgs check roles
// Validate that members exist in the organisation.
memberIds.forEach((memberId) => {
const member = organisation.members.find(({ id }) => id === memberId);
if (!member) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
});
await prisma.$transaction(async (tx) => {
const group = await tx.organisationGroup.create({
data: {
organisationId,
name,
type: OrganisationGroupType.CUSTOM,
organisationRole,
},
});
await tx.organisationGroupMember.createMany({
data: memberIds.map((memberId) => ({
organisationMemberId: memberId,
groupId: group.id,
})),
});
return group;
});
});

View File

@ -0,0 +1,21 @@
import { OrganisationMemberRole } from '@prisma/client';
import { z } from 'zod';
// export const createOrganisationGroupMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/organisation/{teamId}/groups',
// summary: 'Create organisation group',
// description: 'Create a new group for a organisation',
// tags: ['Organisation'],
// },
// };
export const ZCreateOrganisationGroupRequestSchema = z.object({
organisationId: z.string(),
organisationRole: z.nativeEnum(OrganisationMemberRole),
name: z.string().max(100),
memberIds: z.array(z.string()),
});
export const ZCreateOrganisationGroupResponseSchema = z.void();

View File

@ -0,0 +1,23 @@
import { createOrganisationMemberInvites } from '@documenso/lib/server-only/organisation/create-organisation-member-invites';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateOrganisationMemberInvitesRequestSchema,
ZCreateOrganisationMemberInvitesResponseSchema,
} from './create-organisation-member-invites.types';
export const createOrganisationMemberInvitesRoute = authenticatedProcedure
.input(ZCreateOrganisationMemberInvitesRequestSchema)
.output(ZCreateOrganisationMemberInvitesResponseSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId, invitations } = input;
const userId = ctx.user.id;
const userName = ctx.user.name || '';
await createOrganisationMemberInvites({
userId,
userName,
organisationId,
invitations,
});
});

View File

@ -0,0 +1,33 @@
import { OrganisationMemberRole } from '@prisma/client';
import { z } from 'zod';
// export const createOrganisationMemberInvitesMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/organisation/member/create',
// summary: 'Invite organisation members',
// description: 'Invite a users to be part of your organisation',
// tags: ['Organisation'],
// },
// };
export const ZCreateOrganisationMemberInvitesRequestSchema = z.object({
organisationId: z.string().describe('The organisation to invite the user to'),
invitations: z
.array(
z.object({
email: z.string().trim().email().toLowerCase(),
organisationRole: z.nativeEnum(OrganisationMemberRole),
}),
)
// Todo: orgs this probably doesn't work
.refine((items) => new Set(items).size === items.length, {
message: 'Emails must be unique, no duplicate values allowed',
}),
});
export const ZCreateOrganisationMemberInvitesResponseSchema = z.void();
export type TCreateOrganisationMemberInvitesRequestSchema = z.infer<
typeof ZCreateOrganisationMemberInvitesRequestSchema
>;

View File

@ -0,0 +1,22 @@
import { createOrganisation } from '@documenso/lib/server-only/organisation/create-organisation';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateOrganisationRequestSchema,
ZCreateOrganisationResponseSchema,
} from './create-organisation.types';
export const createOrganisationRoute = authenticatedProcedure
// .meta(createOrganisationMeta)
.input(ZCreateOrganisationRequestSchema)
.output(ZCreateOrganisationResponseSchema)
.mutation(async ({ input, ctx }) => {
const { name, url } = input;
const { user } = ctx;
await createOrganisation({
userId: user.id,
name,
url,
});
});

View File

@ -0,0 +1,25 @@
import { z } from 'zod';
import { ZTeamUrlSchema } from '../team-router/schema';
// export const createOrganisationMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/organisation',
// summary: 'Create organisation',
// description: 'Create an organisation',
// tags: ['Organisation'],
// },
// };
export const ZOrganisationNameSchema = z
.string()
.min(3, { message: 'Minimum 3 characters' })
.max(50, { message: 'Maximum 50 characters' });
export const ZCreateOrganisationRequestSchema = z.object({
name: ZOrganisationNameSchema,
url: ZTeamUrlSchema,
});
export const ZCreateOrganisationResponseSchema = z.void();

View File

@ -0,0 +1,38 @@
import { OrganisationMemberInviteStatus } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { maybeAuthenticatedProcedure } from '../trpc';
import {
ZDeclineOrganisationMemberInviteRequestSchema,
ZDeclineOrganisationMemberInviteResponseSchema,
} from './decline-organisation-member-invite.types';
export const declineOrganisationMemberInviteRoute = maybeAuthenticatedProcedure
.input(ZDeclineOrganisationMemberInviteRequestSchema)
.output(ZDeclineOrganisationMemberInviteResponseSchema)
.mutation(async ({ input }) => {
const { token } = input;
const organisationMemberInvite = await prisma.organisationMemberInvite.findFirst({
where: {
token,
},
});
if (!organisationMemberInvite) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
await prisma.organisationMemberInvite.update({
where: {
id: organisationMemberInvite.id,
},
data: {
status: OrganisationMemberInviteStatus.DECLINED,
},
});
// TODO: notify the team owner
});

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZDeclineOrganisationMemberInviteRequestSchema = z.object({
token: z.string(),
});
export const ZDeclineOrganisationMemberInviteResponseSchema = z.void();
export type TDeclineOrganisationMemberInviteResponse = z.infer<
typeof ZDeclineOrganisationMemberInviteResponseSchema
>;

View File

@ -0,0 +1,64 @@
import { OrganisationGroupType } from '@prisma/client';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteOrganisationGroupRequestSchema,
ZDeleteOrganisationGroupResponseSchema,
} from './delete-organisation-group.types';
export const deleteOrganisationGroupRoute = authenticatedProcedure
// .meta(deleteOrganisationGroupMeta)
.input(ZDeleteOrganisationGroupRequestSchema)
.output(ZDeleteOrganisationGroupResponseSchema)
.mutation(async ({ input, ctx }) => {
const { groupId, organisationId } = input;
const { user } = ctx;
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
user.id,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
});
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
const group = await prisma.organisationGroup.findFirst({
where: {
id: groupId,
organisation: {
id: organisationId,
},
},
});
if (!group) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation group not found',
});
}
if (
group.type === OrganisationGroupType.INTERNAL_ORGANISATION ||
group.type === OrganisationGroupType.INTERNAL_TEAM
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not allowed to delete internal groups',
});
}
await prisma.organisationGroup.delete({
where: {
id: groupId,
organisationId: organisation.id,
},
});
});

View File

@ -0,0 +1,18 @@
import { z } from 'zod';
// export const deleteOrganisationGroupMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/organisation/groups/{id}/delete',
// summary: 'Delete organisation group',
// description: 'Delete an existing group for a organisation',
// tags: ['Organisation'],
// },
// };
export const ZDeleteOrganisationGroupRequestSchema = z.object({
organisationId: z.string(),
groupId: z.string(),
});
export const ZDeleteOrganisationGroupResponseSchema = z.void();

View File

@ -0,0 +1,40 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteOrganisationMemberInvitesRequestSchema,
ZDeleteOrganisationMemberInvitesResponseSchema,
} from './delete-organisation-member-invites.types';
export const deleteOrganisationMemberInvitesRoute = authenticatedProcedure
// .meta(deleteOrganisationMemberInvitesMeta)
.input(ZDeleteOrganisationMemberInvitesRequestSchema)
.output(ZDeleteOrganisationMemberInvitesResponseSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId, invitationIds } = input;
const userId = ctx.user.id;
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
await prisma.organisationMemberInvite.deleteMany({
where: {
id: {
in: invitationIds,
},
organisationId,
},
});
});

View File

@ -0,0 +1,20 @@
import { z } from 'zod';
// export const deleteOrganisationMemberInvitesMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/organisation/member/delete-many',
// summary: 'Delete organisation member invites',
// description: 'Delete organisation member invites',
// tags: ['Organisation'],
// },
// };
export const ZDeleteOrganisationMemberInvitesRequestSchema = z.object({
organisationId: z.string(),
invitationIds: z.array(z.string()).refine((items) => new Set(items).size === items.length, {
message: 'Invitation IDs must be unique, no duplicate values allowed',
}),
});
export const ZDeleteOrganisationMemberInvitesResponseSchema = z.void();

View File

@ -0,0 +1,21 @@
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteOrganisationMemberRequestSchema,
ZDeleteOrganisationMemberResponseSchema,
} from './delete-organisation-member.types';
import { deleteOrganisationMembers } from './delete-organisation-members';
export const deleteOrganisationMemberRoute = authenticatedProcedure
// .meta(deleteOrganisationMemberMeta)
.input(ZDeleteOrganisationMemberRequestSchema)
.output(ZDeleteOrganisationMemberResponseSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId, organisationMemberId } = input;
const userId = ctx.user.id;
await deleteOrganisationMembers({
userId,
organisationId,
organisationMemberIds: [organisationMemberId],
});
});

View File

@ -0,0 +1,18 @@
import { z } from 'zod';
// export const deleteOrganisationMemberMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/organisation/member/delete',
// summary: 'Delete organisation member',
// description: 'Delete organisation member',
// tags: ['Organisation'],
// },
// };
export const ZDeleteOrganisationMemberRequestSchema = z.object({
organisationId: z.string(),
organisationMemberId: z.string(),
});
export const ZDeleteOrganisationMemberResponseSchema = z.void();

View File

@ -0,0 +1,105 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteOrganisationMembersRequestSchema,
ZDeleteOrganisationMembersResponseSchema,
} from './delete-organisation-members.types';
export const deleteOrganisationMembersRoute = authenticatedProcedure
// .meta(deleteOrganisationMembersMeta)
.input(ZDeleteOrganisationMembersRequestSchema)
.output(ZDeleteOrganisationMembersResponseSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId, organisationMemberIds } = input;
const userId = ctx.user.id;
await deleteOrganisationMembers({
userId,
organisationId,
organisationMemberIds,
});
});
type DeleteOrganisationMembersProps = {
userId: number;
organisationId: string;
organisationMemberIds: string[];
};
/**
* Deletes multiple organisation members.
*
* This logic is also used to leave a team (hence strange logic).
*/
export const deleteOrganisationMembers = async ({
userId,
organisationId,
organisationMemberIds,
}: DeleteOrganisationMembersProps) => {
const membersToDelete = await prisma.organisationMember.findMany({
where: {
id: {
in: organisationMemberIds,
},
organisationId,
},
});
// Prevent the user from deleting other users if they do not have permission.
if (membersToDelete.some((member) => member.userId !== userId)) {
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
});
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
}
// Todo: Orgs - Handle seats.
await prisma.$transaction(
async (tx) => {
await tx.organisationMember.deleteMany({
where: {
id: {
in: organisationMemberIds,
},
organisationId,
},
});
// Todo: orgs handle removing groups
// if (IS_BILLING_ENABLED() && team.subscription) {
// const numberOfSeats = await tx.teamMember.count({
// where: {
// teamId,
// },
// });
// await updateSubscriptionItemQuantity({
// priceId: team.subscription.priceId,
// subscriptionId: team.subscription.planId,
// quantity: numberOfSeats,
// });
// }
// await jobs.triggerJob({
// name: 'send.team-member-left.email',
// payload: {
// teamId,
// memberUserId: leavingUser.id,
// },
// });
},
{ timeout: 30_000 },
);
};

View File

@ -0,0 +1,22 @@
import { z } from 'zod';
// export const deleteOrganisationMembersMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/organisation/member/delete-many',
// summary: 'Delete organisation members',
// description: 'Delete organisation members',
// tags: ['Organisation'],
// },
// };
export const ZDeleteOrganisationMembersRequestSchema = z.object({
organisationId: z.string(),
organisationMemberIds: z
.array(z.string())
.refine((items) => new Set(items).size === items.length, {
message: 'Organisation member ids must be unique, no duplicate values allowed',
}),
});
export const ZDeleteOrganisationMembersResponseSchema = z.void();

View File

@ -0,0 +1,54 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createPersonalOrganisation } from '@documenso/lib/server-only/organisation/create-organisation';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteOrganisationRequestSchema,
ZDeleteOrganisationResponseSchema,
} from './delete-organisation.types';
export const deleteOrganisationRoute = authenticatedProcedure
// .meta(deleteOrganisationMeta)
.input(ZDeleteOrganisationRequestSchema)
.output(ZDeleteOrganisationResponseSchema)
.mutation(async ({ input, ctx }) => {
const { organisationId } = input;
const { user } = ctx;
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
user.id,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['DELETE_ORGANISATION'],
),
});
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not authorized to delete this organisation',
});
}
const numberOfOrganisationsOwnerHas = await prisma.organisation.count({
where: {
ownerUserId: organisation.ownerUserId,
},
});
// Create an empty organisation for owner since their only one is being deleted.
if (numberOfOrganisationsOwnerHas === 1) {
await createPersonalOrganisation({
userId: organisation.ownerUserId,
throwErrorOnOrganisationCreationFailure: true,
});
}
await prisma.organisation.delete({
where: {
id: organisation.id,
},
});
});

View File

@ -0,0 +1,17 @@
import { z } from 'zod';
// export const deleteOrganisationMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'DELETE',
// path: '/organisation/{teamId}',
// summary: 'Delete organisation',
// description: 'Delete an existing organisation',
// tags: ['Organisation'],
// },
// };
export const ZDeleteOrganisationRequestSchema = z.object({
organisationId: z.string(),
});
export const ZDeleteOrganisationResponseSchema = z.void();

View File

@ -0,0 +1,170 @@
import type { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZFindOrganisationGroupsRequestSchema,
ZFindOrganisationGroupsResponseSchema,
} from './find-organisation-groups.types';
export const findOrganisationGroupsRoute = authenticatedProcedure
// .meta(getOrganisationGroupsMeta)
.input(ZFindOrganisationGroupsRequestSchema)
.output(ZFindOrganisationGroupsResponseSchema)
.query(async ({ input, ctx }) => {
const { organisationId, types, query, page, perPage, organisationGroupId, organisationRoles } =
input;
const { user } = ctx;
return await findOrganisationGroups({
userId: user.id,
organisationId,
organisationGroupId,
organisationRoles,
types,
query,
page,
perPage,
});
});
type FindOrganisationGroupsOptions = {
userId: number;
organisationId: string;
organisationGroupId?: string;
organisationRoles?: OrganisationMemberRole[];
types?: OrganisationGroupType[];
query?: string;
page?: number;
perPage?: number;
};
export const findOrganisationGroups = async ({
userId,
organisationId,
organisationGroupId,
organisationRoles = [],
types = [],
query,
page = 1,
perPage = 10,
}: FindOrganisationGroupsOptions) => {
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
// Todo: Should be fine right?
// ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const whereClause: Prisma.OrganisationGroupWhereInput = {
organisationId: organisation.id,
type:
types.length > 0
? {
in: types,
}
: undefined,
organisationRole:
organisationRoles.length > 0
? {
in: organisationRoles,
}
: undefined,
id: organisationGroupId,
};
if (query) {
whereClause.name = {
contains: query,
mode: Prisma.QueryMode.insensitive,
};
}
const [data, count] = await Promise.all([
prisma.organisationGroup.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
name: 'desc',
},
select: {
id: true,
name: true,
type: true,
organisationId: true,
organisationRole: true,
teamGroups: {
select: {
id: true,
teamId: true,
teamRole: true,
team: {
select: {
id: true,
name: true,
},
},
},
},
organisationGroupMembers: {
select: {
organisationMember: {
select: {
id: true,
user: {
select: {
id: true,
email: true,
name: true,
avatarImageId: true,
},
},
},
},
},
},
},
}),
prisma.organisationGroup.count({
where: whereClause,
}),
]);
const mappedData = data.map((group) => ({
...group,
teams: group.teamGroups.map((teamGroup) => ({
id: teamGroup.team.id,
name: teamGroup.team.name,
teamGroupId: teamGroup.id,
teamRole: teamGroup.teamRole,
})),
members: group.organisationGroupMembers.map(({ organisationMember }) => ({
id: organisationMember.id,
userId: organisationMember.user.id,
name: organisationMember.user.name || '',
email: organisationMember.user.email,
avatarImageId: organisationMember.user.avatarImageId,
})),
}));
return {
data: mappedData,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof mappedData>;
};

View File

@ -0,0 +1,54 @@
import { OrganisationGroupType, OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
import { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { OrganisationGroupSchema } from '@documenso/prisma/generated/zod/modelSchema/OrganisationGroupSchema';
// export const getOrganisationGroupsMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'GET',
// path: '/organisation/{teamId}/groups',
// summary: 'Get organisation groups',
// description: 'Get all groups for a organisation',
// tags: ['Organisation'],
// },
// };
export const ZFindOrganisationGroupsRequestSchema = ZFindSearchParamsSchema.extend({
organisationId: z.string(),
organisationGroupId: z.string().optional(),
organisationRoles: z.nativeEnum(OrganisationMemberRole).array().optional(),
types: z.nativeEnum(OrganisationGroupType).array().optional(),
});
export const ZFindOrganisationGroupsResponseSchema = ZFindResultResponse.extend({
data: OrganisationGroupSchema.pick({
type: true,
organisationRole: true,
id: true,
name: true,
organisationId: true,
})
.extend({
members: z
.object({
id: z.string(),
userId: z.number(),
name: z.string(),
email: z.string(),
avatarImageId: z.string().nullable(),
})
.array(),
teams: z
.object({
id: z.number(),
name: z.string(),
teamGroupId: z.string(),
teamRole: z.nativeEnum(TeamMemberRole),
})
.array(),
})
.array(),
});
export type TFindOrganisationGroupsResponse = z.infer<typeof ZFindOrganisationGroupsResponseSchema>;

View File

@ -0,0 +1,105 @@
import type { OrganisationMemberInviteStatus } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZFindOrganisationMemberInvitesRequestSchema,
ZFindOrganisationMemberInvitesResponseSchema,
} from './find-organisation-member-invites.types';
export const findOrganisationMemberInvitesRoute = authenticatedProcedure
// .meta(getOrganisationMemberInvitesMeta)
.input(ZFindOrganisationMemberInvitesRequestSchema)
.output(ZFindOrganisationMemberInvitesResponseSchema)
.query(async ({ input, ctx }) => {
const { organisationId, query, page, perPage, status } = input;
const { user } = ctx;
return await findOrganisationMemberInvites({
userId: user.id,
organisationId,
query,
page,
perPage,
status,
});
});
type FindOrganisationMemberInvitesOptions = {
userId: number;
organisationId: string;
query?: string;
page?: number;
perPage?: number;
status?: OrganisationMemberInviteStatus;
};
export const findOrganisationMemberInvites = async ({
userId,
organisationId,
query,
page = 1,
perPage = 10,
status,
}: FindOrganisationMemberInvitesOptions) => {
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const whereClause: Prisma.OrganisationMemberInviteWhereInput = {
organisationId: organisation.id,
status,
};
if (query) {
whereClause.email = {
contains: query,
mode: Prisma.QueryMode.insensitive,
};
}
const [data, count] = await Promise.all([
prisma.organisationMemberInvite.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
createdAt: 'desc',
},
// Exclude token attribute.
select: {
id: true,
organisationId: true,
email: true,
createdAt: true,
organisationRole: true,
status: true,
},
}),
prisma.organisationMemberInvite.count({
where: whereClause,
}),
]);
return {
data,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof data>;
};

View File

@ -0,0 +1,35 @@
import { OrganisationMemberInviteStatus } from '@prisma/client';
import { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { OrganisationMemberInviteSchema } from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberInviteSchema';
// export const getOrganisationMemberInvitesMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'GET',
// path: '/organisation/{teamId}/members/pending',
// summary: 'Find organisation members pending',
// description: 'Find all members of a organisation pending',
// tags: ['Organisation'],
// },
// };
export const ZFindOrganisationMemberInvitesRequestSchema = ZFindSearchParamsSchema.extend({
organisationId: z.string(),
status: z.nativeEnum(OrganisationMemberInviteStatus).optional(),
});
export const ZFindOrganisationMemberInvitesResponseSchema = ZFindResultResponse.extend({
data: OrganisationMemberInviteSchema.pick({
id: true,
organisationId: true,
email: true,
createdAt: true,
organisationRole: true,
status: true,
}).array(),
});
export type TFindOrganisationMemberInvitesResponse = z.infer<
typeof ZFindOrganisationMemberInvitesResponseSchema
>;

View File

@ -0,0 +1,137 @@
import { Prisma } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import {
buildOrganisationWhereQuery,
getHighestOrganisationRoleInGroup,
} from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZFindOrganisationMembersRequestSchema,
ZFindOrganisationMembersResponseSchema,
} from './find-organisation-members.types';
export const findOrganisationMembersRoute = authenticatedProcedure
// .meta(getOrganisationMembersMeta)
.input(ZFindOrganisationMembersRequestSchema)
.output(ZFindOrganisationMembersResponseSchema)
.query(async ({ input, ctx }) => {
const { organisationId } = input;
const { id } = ctx.user;
const organisationMembers = await findOrganisationMembers({
userId: id,
organisationId,
query: input.query,
page: input.page,
perPage: input.perPage,
});
return {
...organisationMembers,
data: organisationMembers.data.map((organisationMember) => {
const groups = organisationMember.organisationGroupMembers.map((group) => group.group);
return {
id: organisationMember.id,
userId: organisationMember.user.id,
email: organisationMember.user.email,
name: organisationMember.user.name || '',
createdAt: organisationMember.createdAt,
currentOrganisationRole: getHighestOrganisationRoleInGroup(groups),
avatarImageId: organisationMember.user.avatarImageId,
groups,
};
}),
};
});
type FindOrganisationMembersOptions = {
userId: number;
organisationId: string;
query?: string;
page?: number;
perPage?: number;
};
export const findOrganisationMembers = async ({
userId,
organisationId,
query,
page = 1,
perPage = 10,
}: FindOrganisationMembersOptions) => {
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(organisationId, userId),
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const whereClause: Prisma.OrganisationMemberWhereInput = {
organisationId: organisation.id,
};
if (query) {
whereClause.user = {
OR: [
{
email: {
contains: query,
mode: Prisma.QueryMode.insensitive,
},
},
{
name: {
contains: query,
mode: Prisma.QueryMode.insensitive,
},
},
],
};
}
const [data, count] = await Promise.all([
prisma.organisationMember.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
organisationId: true,
user: {
select: {
id: true,
email: true,
name: true,
avatarImageId: true,
},
},
organisationGroupMembers: {
select: {
group: true,
},
},
createdAt: true,
},
}),
prisma.organisationMember.count({
where: whereClause,
}),
]);
return {
data,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof data>;
};

View File

@ -0,0 +1,46 @@
import { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import OrganisationMemberRoleSchema from '@documenso/prisma/generated/zod/inputTypeSchemas/OrganisationMemberRoleSchema';
import OrganisationGroupSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationGroupSchema';
import { OrganisationMemberSchema } from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberSchema';
// export const getOrganisationMembersMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'GET',
// path: '/organisation/{teamId}/members',
// summary: 'Find organisation members',
// description: 'Find all members of a organisation',
// tags: ['Organisation'],
// },
// };
export const ZFindOrganisationMembersRequestSchema = ZFindSearchParamsSchema.extend({
organisationId: z.string(),
});
export const ZFindOrganisationMembersResponseSchema = ZFindResultResponse.extend({
data: OrganisationMemberSchema.pick({
id: true,
createdAt: true,
userId: true,
})
.extend({
email: z.string(),
name: z.string(),
avatarImageId: z.string().nullable(),
currentOrganisationRole: OrganisationMemberRoleSchema,
groups: z.array(
OrganisationGroupSchema.pick({
id: true,
organisationRole: true,
type: true,
}),
),
})
.array(),
});
export type TFindOrganisationMembersResponse = z.infer<
typeof ZFindOrganisationMembersResponseSchema
>;

View File

@ -0,0 +1,33 @@
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZGetOrganisationMemberInvitesRequestSchema,
ZGetOrganisationMemberInvitesResponseSchema,
} from './get-organisation-member-invites.types';
export const getOrganisationMemberInvitesRoute = authenticatedProcedure
// .meta(getOrganisationMemberInvitesMeta)
.input(ZGetOrganisationMemberInvitesRequestSchema)
.output(ZGetOrganisationMemberInvitesResponseSchema)
.query(async ({ input, ctx }) => {
const { user } = ctx;
const { status } = input;
return await prisma.organisationMemberInvite.findMany({
where: {
email: user.email,
status,
},
include: {
organisation: {
select: {
id: true,
name: true,
avatarImageId: true,
},
},
},
});
});

View File

@ -0,0 +1,29 @@
import { OrganisationMemberInviteStatus } from '@prisma/client';
import { z } from 'zod';
import { OrganisationMemberInviteSchema } from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberInviteSchema';
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
export const ZGetOrganisationMemberInvitesRequestSchema = z.object({
status: z.nativeEnum(OrganisationMemberInviteStatus).optional(),
});
export const ZGetOrganisationMemberInvitesResponseSchema = OrganisationMemberInviteSchema.pick({
id: true,
organisationId: true,
email: true,
createdAt: true,
token: true,
})
.extend({
organisation: OrganisationSchema.pick({
id: true,
name: true,
avatarImageId: true,
}),
})
.array();
export type TGetOrganisationMemberInvitesResponse = z.infer<
typeof ZGetOrganisationMemberInvitesResponseSchema
>;

View File

@ -0,0 +1,91 @@
import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisations';
import { getHighestTeamRoleInGroup } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import type { TGetOrganisationSessionResponse } from './get-organisation-session.types';
import { ZGetOrganisationSessionResponseSchema } from './get-organisation-session.types';
/**
* Get all the organisations and teams a user belongs to.
*/
export const getOrganisationSessionRoute = authenticatedProcedure
.output(ZGetOrganisationSessionResponseSchema)
.query(async ({ ctx }) => {
return await getOrganisationSession({ userId: ctx.user.id });
});
export const getOrganisationSession = async ({
userId,
}: {
userId: number;
}): Promise<TGetOrganisationSessionResponse> => {
const organisations = await prisma.organisation.findMany({
where: {
members: {
some: {
userId,
},
},
},
include: {
groups: {
where: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
teams: {
where: {
teamGroups: {
some: {
organisationGroup: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
},
},
include: {
teamGroups: {
where: {
organisationGroup: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
include: {
organisationGroup: true,
},
},
},
},
},
});
return organisations.map((organisation) => {
return {
...organisation,
teams: organisation.teams.map((team) => ({
...team,
currentTeamRole: getHighestTeamRoleInGroup(team.teamGroups),
})),
currentOrganisationRole: getHighestOrganisationRoleInGroup(organisation.groups),
};
});
};

View File

@ -0,0 +1,26 @@
import { z } from 'zod';
import { ZOrganisationSchema } from '@documenso/lib/types/organisation';
import { OrganisationMemberRole, TeamMemberRole } from '@documenso/prisma/generated/types';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
export const ZGetOrganisationSessionResponseSchema = ZOrganisationSchema.extend({
teams: z.array(
TeamSchema.pick({
id: true,
name: true,
url: true,
createdAt: true,
avatarImageId: true,
organisationId: true,
}).extend({
currentTeamRole: z.nativeEnum(TeamMemberRole),
}),
),
currentOrganisationRole: z.nativeEnum(OrganisationMemberRole),
}).array();
export type TGetOrganisationSessionResponse = z.infer<typeof ZGetOrganisationSessionResponseSchema>;
export type TeamSession = TGetOrganisationSessionResponse[number]['teams'][number];
export type OrganisationSession = TGetOrganisationSessionResponse[number];

View File

@ -0,0 +1,77 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZGetOrganisationRequestSchema,
ZGetOrganisationResponseSchema,
} from './get-organisation.types';
export const getOrganisationRoute = authenticatedProcedure
// .meta(getOrganisationMeta)
.input(ZGetOrganisationRequestSchema)
.output(ZGetOrganisationResponseSchema)
.query(async ({ input, ctx }) => {
const { organisationReference } = input;
return await getOrganisation({
userId: ctx.user.id,
organisationReference,
});
});
type GetOrganisationOptions = {
userId: number;
/**
* The ID or URL of the organisation.
*/
organisationReference: string;
};
export const getOrganisation = async ({
userId,
organisationReference,
}: GetOrganisationOptions) => {
const organisation = await prisma.organisation.findFirst({
where: {
OR: [{ id: organisationReference }, { url: organisationReference }],
members: {
some: {
userId,
},
},
},
include: {
organisationGlobalSettings: true,
teams: {
where: {
teamGroups: {
some: {
organisationGroup: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
},
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation not found',
});
}
return {
...organisation,
teams: organisation.teams,
};
};

View File

@ -0,0 +1,35 @@
import { z } from 'zod';
import { ZOrganisationSchema } from '@documenso/lib/types/organisation';
import OrganisationGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationGlobalSettingsSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
// export const getOrganisationMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'GET',
// path: '/organisation/{teamReference}',
// summary: 'Get organisation',
// description: 'Get an organisation by ID or URL',
// tags: ['Organisation'],
// },
// };
export const ZGetOrganisationRequestSchema = z.object({
organisationReference: z.string().describe('The ID or URL of the organisation.'),
});
export const ZGetOrganisationResponseSchema = ZOrganisationSchema.extend({
organisationGlobalSettings: OrganisationGlobalSettingsSchema,
teams: z.array(
TeamSchema.pick({
id: true,
name: true,
url: true,
createdAt: true,
avatarImageId: true,
organisationId: true,
}),
),
});
export type TGetOrganisationResponse = z.infer<typeof ZGetOrganisationResponseSchema>;

View File

@ -0,0 +1,58 @@
import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZGetOrganisationsRequestSchema,
ZGetOrganisationsResponseSchema,
} from './get-organisations.types';
export const getOrganisationsRoute = authenticatedProcedure
// .meta(getOrganisationsMeta)
.input(ZGetOrganisationsRequestSchema)
.output(ZGetOrganisationsResponseSchema)
.query(async ({ ctx }) => {
const { user } = ctx;
return getOrganisations({ userId: user.id });
});
export const getOrganisations = async ({ userId }: { userId: number }) => {
const organisations = await prisma.organisation.findMany({
where: {
members: {
some: {
userId,
},
},
},
include: {
members: {
where: {
userId,
},
},
groups: {
where: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
},
});
return organisations.map(({ groups, ...organisation }) => {
const currentOrganisationRole = getHighestOrganisationRoleInGroup(groups);
return {
...organisation,
currentOrganisationRole: currentOrganisationRole,
currentMemberId: organisation.members[0].id,
};
});
};

View File

@ -0,0 +1,23 @@
import { z } from 'zod';
import { ZOrganisationManySchema } from '@documenso/lib/types/organisation';
import OrganisationMemberRoleSchema from '@documenso/prisma/generated/zod/inputTypeSchemas/OrganisationMemberRoleSchema';
// export const getOrganisationsMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'GET',
// path: '/organisation/teams',
// summary: 'Get teams',
// description: 'Get all teams you are a member of',
// tags: ['Organisation'],
// },
// };
export const ZGetOrganisationsRequestSchema = z.void();
export const ZGetOrganisationsResponseSchema = ZOrganisationManySchema.extend({
currentOrganisationRole: OrganisationMemberRoleSchema,
currentMemberId: z.string(),
}).array();
export type TGetOrganisationsResponse = z.infer<typeof ZGetOrganisationsResponseSchema>;

View File

@ -0,0 +1,99 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { sendOrganisationMemberInviteEmail } from '@documenso/lib/server-only/organisation/create-organisation-member-invites';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZResendOrganisationMemberInviteRequestSchema,
ZResendOrganisationMemberInviteResponseSchema,
} from './resend-organisation-member-invite.types';
export const resendOrganisationMemberInviteRoute = authenticatedProcedure
// .meta(resendOrganisationMemberInviteMeta)
.input(ZResendOrganisationMemberInviteRequestSchema)
.output(ZResendOrganisationMemberInviteResponseSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId, invitationId } = input;
const userId = ctx.user.id;
const userName = ctx.user.name || '';
await resendOrganisationMemberInvitation({
userId,
userName,
organisationId,
invitationId,
});
});
export type ResendOrganisationMemberInvitationOptions = {
/**
* The ID of the user who is initiating this action.
*/
userId: number;
/**
* The name of the user who is initiating this action.
*/
userName: string;
/**
* The ID of the organisation.
*/
organisationId: string;
/**
* The IDs of the invitations to resend.
*/
invitationId: string;
};
/**
* Resend an email for a given member invite.
*/
export const resendOrganisationMemberInvitation = async ({
userId,
userName,
organisationId,
invitationId,
}: ResendOrganisationMemberInvitationOptions): Promise<void> => {
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
include: {
organisationGlobalSettings: true,
invites: {
where: {
id: invitationId,
},
},
},
});
if (!organisation) {
throw new AppError('OrganisationNotFound', {
message: 'User is not a valid member of the team.',
statusCode: 404,
});
}
const invitation = organisation.invites[0];
if (!invitation) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Invitation does not exist',
});
}
await sendOrganisationMemberInviteEmail({
email: invitation.email,
token: invitation.token,
senderName: userName,
organisation,
});
};

View File

@ -0,0 +1,18 @@
import { z } from 'zod';
// export const resendOrganisationMemberInviteMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/organisation/member/resend-invite',
// summary: 'Resend organisation member invite',
// description: 'Resend a organisation member invite',
// tags: ['Organisation'],
// },
// };
export const ZResendOrganisationMemberInviteRequestSchema = z.object({
organisationId: z.string(),
invitationId: z.string(),
});
export const ZResendOrganisationMemberInviteResponseSchema = z.void();

View File

@ -0,0 +1,58 @@
import { router } from '../trpc';
import { acceptOrganisationMemberInviteRoute } from './accept-organisation-member-invite';
import { createOrganisationRoute } from './create-organisation';
import { createOrganisationGroupRoute } from './create-organisation-group';
import { createOrganisationMemberInvitesRoute } from './create-organisation-member-invites';
import { declineOrganisationMemberInviteRoute } from './decline-organisation-member-invite';
import { deleteOrganisationRoute } from './delete-organisation';
import { deleteOrganisationGroupRoute } from './delete-organisation-group';
import { deleteOrganisationMemberRoute } from './delete-organisation-member';
import { deleteOrganisationMemberInvitesRoute } from './delete-organisation-member-invites';
import { deleteOrganisationMembersRoute } from './delete-organisation-members';
import { findOrganisationGroupsRoute } from './find-organisation-groups';
import { findOrganisationMemberInvitesRoute } from './find-organisation-member-invites';
import { findOrganisationMembersRoute } from './find-organisation-members';
import { getOrganisationRoute } from './get-organisation';
import { getOrganisationMemberInvitesRoute } from './get-organisation-member-invites';
import { getOrganisationSessionRoute } from './get-organisation-session';
import { getOrganisationsRoute } from './get-organisations';
import { resendOrganisationMemberInviteRoute } from './resend-organisation-member-invite';
import { updateOrganisationRoute } from './update-organisation';
import { updateOrganisationGroupRoute } from './update-organisation-group';
import { updateOrganisationMemberRoute } from './update-organisation-members';
import { updateOrganisationSettingsRoute } from './update-organisation-settings';
export const organisationRouter = router({
get: getOrganisationRoute,
getMany: getOrganisationsRoute,
create: createOrganisationRoute,
update: updateOrganisationRoute,
delete: deleteOrganisationRoute,
member: {
find: findOrganisationMembersRoute,
update: updateOrganisationMemberRoute,
delete: deleteOrganisationMemberRoute,
deleteMany: deleteOrganisationMembersRoute,
invite: {
find: findOrganisationMemberInvitesRoute,
getMany: getOrganisationMemberInvitesRoute,
createMany: createOrganisationMemberInvitesRoute,
deleteMany: deleteOrganisationMemberInvitesRoute,
accept: acceptOrganisationMemberInviteRoute,
decline: declineOrganisationMemberInviteRoute,
resend: resendOrganisationMemberInviteRoute,
},
},
group: {
find: findOrganisationGroupsRoute,
create: createOrganisationGroupRoute,
update: updateOrganisationGroupRoute,
delete: deleteOrganisationGroupRoute,
},
settings: {
update: updateOrganisationSettingsRoute,
},
internal: {
getOrganisationSession: getOrganisationSessionRoute,
},
});

View File

@ -0,0 +1,125 @@
import { unique } from 'remeda';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getMemberOrganisationRole } from '@documenso/lib/server-only/team/get-member-roles';
import {
buildOrganisationWhereQuery,
isOrganisationRoleWithinUserHierarchy,
} from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { OrganisationGroupType } from '@documenso/prisma/generated/types';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateOrganisationGroupRequestSchema,
ZUpdateOrganisationGroupResponseSchema,
} from './update-organisation-group.types';
export const updateOrganisationGroupRoute = authenticatedProcedure
// .meta(updateOrganisationGroupMeta)
.input(ZUpdateOrganisationGroupRequestSchema)
.output(ZUpdateOrganisationGroupResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id, ...data } = input;
const { user } = ctx;
const organisationGroup = await prisma.organisationGroup.findFirst({
where: {
id,
organisation: buildOrganisationWhereQuery(
undefined,
user.id,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
},
include: {
organisationGroupMembers: true,
},
});
if (!organisationGroup) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation group not found',
});
}
if (organisationGroup.type === OrganisationGroupType.INTERNAL_ORGANISATION) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not allowed to update internal organisation groups',
});
}
const currentUserOrganisationRole = await getMemberOrganisationRole({
organisationId: organisationGroup.organisationId,
reference: {
type: 'User',
id: user.id,
},
});
if (
!isOrganisationRoleWithinUserHierarchy(
currentUserOrganisationRole,
organisationGroup.organisationRole,
)
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not allowed to update this organisation group',
});
}
if (
data.organisationRole &&
!isOrganisationRoleWithinUserHierarchy(currentUserOrganisationRole, data.organisationRole)
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not allowed to set an organisation role higher than your own',
});
}
const groupMemberIds = unique(data.memberIds || []);
const membersToDelete = organisationGroup.organisationGroupMembers.filter(
(member) => !groupMemberIds.includes(member.organisationMemberId),
);
const membersToCreate = groupMemberIds.filter(
(id) =>
!organisationGroup.organisationGroupMembers.some(
(member) => member.organisationMemberId === id,
),
);
await prisma.$transaction(async (tx) => {
await tx.organisationGroup.update({
where: {
id,
},
data: {
organisationRole: data.organisationRole,
name: data.name,
},
});
// Only run deletion if memberIds is defined.
if (data.memberIds && membersToDelete.length > 0) {
await tx.organisationGroupMember.deleteMany({
where: {
groupId: organisationGroup.id,
organisationMemberId: { in: membersToDelete.map((m) => m.organisationMemberId) },
},
});
}
// Only run creation if memberIds is defined.
if (data.memberIds && membersToCreate.length > 0) {
await tx.organisationGroupMember.createMany({
data: membersToCreate.map((id) => ({
groupId: organisationGroup.id,
organisationMemberId: id,
})),
});
}
});
});

View File

@ -0,0 +1,24 @@
import { OrganisationMemberRole } from '@prisma/client';
import { z } from 'zod';
// export const updateOrganisationGroupMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/organisation/groups/{id}',
// summary: 'Update organisation group',
// description: 'Update an existing group for a organisation',
// tags: ['Organisation'],
// requiredScopes: ['personal:organisation:write'],
// },
// };
export const ZUpdateOrganisationGroupRequestSchema = z.object({
id: z.string(),
name: z.string().nullable().optional(),
organisationRole: z.nativeEnum(OrganisationMemberRole).optional(),
memberIds: z.array(z.string()).optional(),
});
export const ZUpdateOrganisationGroupResponseSchema = z.void();
export type TUpdateOrganisationGroupRequest = z.infer<typeof ZUpdateOrganisationGroupRequestSchema>;

View File

@ -0,0 +1,163 @@
import { OrganisationGroupType } from '@prisma/client';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import {
buildOrganisationWhereQuery,
getHighestOrganisationRoleInGroup,
isOrganisationRoleWithinUserHierarchy,
} from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateOrganisationMemberRequestSchema,
ZUpdateOrganisationMemberResponseSchema,
} from './update-organisation-members.types';
export const updateOrganisationMemberRoute = authenticatedProcedure
// .meta(updateOrganisationMemberMeta)
.input(ZUpdateOrganisationMemberRequestSchema)
.output(ZUpdateOrganisationMemberResponseSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId, organisationMemberId, data } = input;
const userId = ctx.user.id;
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
include: {
groups: {
where: {
type: OrganisationGroupType.INTERNAL_ORGANISATION,
},
},
members: {
include: {
organisationGroupMembers: {
include: {
group: true,
},
},
user: {
select: {
id: true,
name: true,
email: true,
},
},
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Organisation not found' });
}
const currentOrganisationMember = organisation.members.find(
(member) => member.userId === userId,
);
const organisationMemberToUpdate = organisation.members.find(
(member) => member.id === organisationMemberId,
);
if (!organisationMemberToUpdate || !currentOrganisationMember) {
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Organisation member does not exist' });
}
if (organisationMemberToUpdate.userId === organisation.ownerUserId) {
throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'Cannot update the owner' });
}
const currentUserOrganisationRoles = currentOrganisationMember.organisationGroupMembers.filter(
({ group }) => group.type === OrganisationGroupType.INTERNAL_ORGANISATION,
);
if (currentUserOrganisationRoles.length !== 1) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Current user has multiple internal organisation roles',
});
}
const currentUserOrganisationRole = currentUserOrganisationRoles[0].group.organisationRole;
const currentMemberToUpdateOrganisationRole = getHighestOrganisationRoleInGroup(
organisationMemberToUpdate.organisationGroupMembers.flatMap((member) => member.group),
);
const isMemberToUpdateHigherRole = !isOrganisationRoleWithinUserHierarchy(
currentUserOrganisationRole,
currentMemberToUpdateOrganisationRole,
);
if (isMemberToUpdateHigherRole) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Cannot update a member with a higher role',
});
}
const isNewMemberRoleHigherThanCurrentRole = !isOrganisationRoleWithinUserHierarchy(
currentUserOrganisationRole,
data.role,
);
if (isNewMemberRoleHigherThanCurrentRole) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Cannot give a member a role higher than the user initating the update',
});
}
const currentMemberGroup = organisation.groups.find(
(group) => group.organisationRole === currentMemberToUpdateOrganisationRole,
);
const newMemberGroup = organisation.groups.find(
(group) => group.organisationRole === data.role,
);
if (!currentMemberGroup) {
console.error('[CRITICAL]: Missing internal group');
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Current member group not found',
});
}
if (!newMemberGroup) {
console.error('[CRITICAL]: Missing internal group');
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'New member group not found',
});
}
// Switch member to new internal group role.
await prisma.$transaction(async (tx) => {
console.log({
organisationMemberId_groupId: {
organisationMemberId: organisationMemberToUpdate.id,
groupId: currentMemberGroup.id,
},
});
await tx.organisationGroupMember.delete({
where: {
organisationMemberId_groupId: {
organisationMemberId: organisationMemberToUpdate.id,
groupId: currentMemberGroup.id,
},
},
});
await tx.organisationGroupMember.create({
data: {
organisationMemberId: organisationMemberToUpdate.id,
groupId: newMemberGroup.id,
},
});
});
});

View File

@ -0,0 +1,22 @@
import { OrganisationMemberRole } from '@prisma/client';
import { z } from 'zod';
// export const updateOrganisationMemberMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/organisation/member/update',
// summary: 'Update organisation member',
// description: 'Update organisation member',
// tags: ['Organisation'],
// },
// };
export const ZUpdateOrganisationMemberRequestSchema = z.object({
organisationId: z.string(),
organisationMemberId: z.string(),
data: z.object({
role: z.nativeEnum(OrganisationMemberRole),
}),
});
export const ZUpdateOrganisationMemberResponseSchema = z.void();

View File

@ -0,0 +1,102 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateOrganisationSettingsRequestSchema,
ZUpdateOrganisationSettingsResponseSchema,
} from './update-organisation-settings.types';
export const updateOrganisationSettingsRoute = authenticatedProcedure
.input(ZUpdateOrganisationSettingsRequestSchema)
.output(ZUpdateOrganisationSettingsResponseSchema)
.mutation(async ({ ctx, input }) => {
const { user } = ctx;
const { organisationId, data } = input;
const {
// Document related settings.
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
// Branding related settings.
brandingEnabled,
brandingLogo,
brandingUrl,
brandingCompanyDetails,
brandingHidePoweredBy,
} = data;
if (Object.values(data).length === 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'No settings to update',
});
}
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
user.id,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
include: {
organisationGlobalSettings: true,
},
});
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update this organisation.',
});
}
const derivedTypedSignatureEnabled =
typedSignatureEnabled ?? organisation.organisationGlobalSettings.typedSignatureEnabled;
const derivedUploadSignatureEnabled =
uploadSignatureEnabled ?? organisation.organisationGlobalSettings.uploadSignatureEnabled;
const derivedDrawSignatureEnabled =
drawSignatureEnabled ?? organisation.organisationGlobalSettings.drawSignatureEnabled;
if (
derivedTypedSignatureEnabled === false &&
derivedUploadSignatureEnabled === false &&
derivedDrawSignatureEnabled === false
) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'At least one signature type must be enabled',
});
}
await prisma.organisation.update({
where: {
id: organisationId,
},
data: {
organisationGlobalSettings: {
update: {
// Document related settings.
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
// Branding related settings.
brandingEnabled,
brandingLogo,
brandingUrl,
brandingCompanyDetails,
brandingHidePoweredBy,
},
},
},
});
});

View File

@ -0,0 +1,27 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
export const ZUpdateOrganisationSettingsRequestSchema = z.object({
organisationId: z.string(),
data: z.object({
// Document related settings.
documentVisibility: z.nativeEnum(DocumentVisibility).optional(),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
includeSenderDetails: z.boolean().optional(),
includeSigningCertificate: z.boolean().optional(),
typedSignatureEnabled: z.boolean().optional(),
uploadSignatureEnabled: z.boolean().optional(),
drawSignatureEnabled: z.boolean().optional(),
// Branding related settings.
brandingEnabled: z.boolean().optional(),
brandingLogo: z.string().optional(),
brandingUrl: z.string().optional(),
brandingCompanyDetails: z.string().optional(),
brandingHidePoweredBy: z.boolean().optional(),
}),
});
export const ZUpdateOrganisationSettingsResponseSchema = z.void();

View File

@ -0,0 +1,45 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateOrganisationRequestSchema,
ZUpdateOrganisationResponseSchema,
} from './update-organisation.types';
export const updateOrganisationRoute = authenticatedProcedure
// .meta(updateOrganisationMeta)
.input(ZUpdateOrganisationRequestSchema)
.output(ZUpdateOrganisationResponseSchema)
.mutation(async ({ input, ctx }) => {
const { organisationId, data } = input;
const userId = ctx.user.id;
// Check if organisation exists and user has access to it
const existingOrganisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
});
if (!existingOrganisation) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation not found',
});
}
await prisma.organisation.update({
where: {
id: organisationId,
},
data: {
name: data.name,
url: data.url, // Todo: (orgs) check url unique
},
});
});

View File

@ -0,0 +1,23 @@
import { z } from 'zod';
import { ZCreateOrganisationRequestSchema } from './create-organisation.types';
// export const updateOrganisationMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/organisation/{teamId}',
// summary: 'Update organisation',
// description: 'Update an organisation',
// tags: ['Organisation'],
// },
// };
export const ZUpdateOrganisationRequestSchema = z.object({
data: ZCreateOrganisationRequestSchema.pick({
name: true,
url: true,
}),
organisationId: z.string(),
});
export const ZUpdateOrganisationResponseSchema = z.void();

View File

@ -1,16 +1,10 @@
import { SubscriptionStatus } from '@prisma/client';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error';
import { setAvatarImage } from '@documenso/lib/server-only/profile/set-avatar-image';
import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
import { createBillingPortal } from '@documenso/lib/server-only/user/create-billing-portal';
import { createCheckoutSession } from '@documenso/lib/server-only/user/create-checkout-session';
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
import { updatePublicProfile } from '@documenso/lib/server-only/user/update-public-profile';
import { adminProcedure, authenticatedProcedure, router } from '../trpc';
import {
@ -19,7 +13,6 @@ import {
ZRetrieveUserByIdQuerySchema,
ZSetProfileImageMutationSchema,
ZUpdateProfileMutationSchema,
ZUpdatePublicProfileMutationSchema,
} from './schema';
export const profileRouter = router({
@ -76,37 +69,6 @@ export const profileRouter = router({
});
}),
updatePublicProfile: authenticatedProcedure
.input(ZUpdatePublicProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
const { url, bio, enabled } = input;
if (IS_BILLING_ENABLED() && url !== undefined && url.length < 6) {
const subscriptions = await getSubscriptionsByUserId({
userId: ctx.user.id,
}).then((subscriptions) =>
subscriptions.filter((s) => s.status === SubscriptionStatus.ACTIVE),
);
if (subscriptions.length === 0) {
throw new AppError('PREMIUM_PROFILE_URL', {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
}
const user = await updatePublicProfile({
userId: ctx.user.id,
data: {
url,
bio,
enabled,
},
});
return { success: true, url: user.url };
}),
deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
return await deleteUser({
id: ctx.user.id,

View File

@ -1,7 +1,5 @@
import { z } from 'zod';
export const MAX_PROFILE_BIO_LENGTH = 256;
export const ZFindUserSecurityAuditLogsSchema = z.object({
page: z.number().optional(),
perPage: z.number().optional(),
@ -26,27 +24,6 @@ export const ZUpdateProfileMutationSchema = z.object({
export type TUpdateProfileMutationSchema = z.infer<typeof ZUpdateProfileMutationSchema>;
export const ZUpdatePublicProfileMutationSchema = z.object({
bio: z
.string()
.max(MAX_PROFILE_BIO_LENGTH, {
message: `Bio must be shorter than ${MAX_PROFILE_BIO_LENGTH + 1} characters`,
})
.optional(),
enabled: z.boolean().optional(),
url: z
.string()
.trim()
.toLowerCase()
.min(1, { message: 'Please enter a valid username.' })
.regex(/^[a-z0-9-]+$/, {
message: 'Username can only container alphanumeric characters and dashes.',
})
.optional(),
});
export type TUpdatePublicProfileMutationSchema = z.infer<typeof ZUpdatePublicProfileMutationSchema>;
export const ZSetProfileImageMutationSchema = z.object({
bytes: z.string().nullish(),
teamId: z.number().min(1).nullish(),

View File

@ -3,6 +3,7 @@ import { apiTokenRouter } from './api-token-router/router';
import { authRouter } from './auth-router/router';
import { documentRouter } from './document-router/router';
import { fieldRouter } from './field-router/router';
import { organisationRouter } from './organisation-router/router';
import { profileRouter } from './profile-router/router';
import { recipientRouter } from './recipient-router/router';
import { shareLinkRouter } from './share-link-router/router';
@ -18,6 +19,7 @@ export const appRouter = router({
field: fieldRouter,
recipient: recipientRouter,
admin: adminRouter,
organisation: organisationRouter,
shareLink: shareLinkRouter,
apiToken: apiTokenRouter,
team: teamRouter,

View File

@ -0,0 +1,101 @@
import {
ALLOWED_TEAM_GROUP_TYPES,
TEAM_MEMBER_ROLE_PERMISSIONS_MAP,
} from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import {
OrganisationGroupType,
OrganisationMemberRole,
TeamMemberRole,
} from '@documenso/prisma/generated/types';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateTeamGroupsRequestSchema,
ZCreateTeamGroupsResponseSchema,
} from './create-team-groups.types';
export const createTeamGroupsRoute = authenticatedProcedure
// .meta(createTeamGroupsMeta)
.input(ZCreateTeamGroupsRequestSchema)
.output(ZCreateTeamGroupsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, groups } = input;
const { user } = ctx;
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery(teamId, user.id, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
include: {
organisation: {
include: {
groups: {
include: {
teamGroups: true,
},
},
},
},
},
});
if (!team) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
const { teamRole: currentUserTeamRole } = await getMemberRoles({
teamId,
reference: {
type: 'User',
id: user.id,
},
});
const isValid = groups.every((group) => {
const organisationGroup = team.organisation.groups.find(
({ id }) => id === group.organisationGroupId,
);
// Only allow specific organisation groups to be used as a reference for team groups.
if (!organisationGroup?.type || !ALLOWED_TEAM_GROUP_TYPES.includes(organisationGroup.type)) {
return false;
}
// The "EVERYONE" organisation group can only have the "TEAM MEMBER" role for now.
if (
organisationGroup.type === OrganisationGroupType.INTERNAL_ORGANISATION &&
organisationGroup.organisationRole === OrganisationMemberRole.MEMBER &&
group.teamRole !== TeamMemberRole.MEMBER
) {
return false;
}
// Check that the group is not already added to the team.
if (organisationGroup.teamGroups.some((teamGroup) => teamGroup.teamId === teamId)) {
return false;
}
// Check that the user has permission to add the group to the team.
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, group.teamRole)) {
return false;
}
return true;
});
if (!isValid) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid groups',
});
}
await prisma.teamGroup.createMany({
data: groups.map((group) => ({
teamId,
organisationGroupId: group.organisationGroupId,
teamRole: group.teamRole,
})),
});
});

View File

@ -0,0 +1,28 @@
import { TeamMemberRole } from '@prisma/client';
import { z } from 'zod';
// export const createTeamGroupsMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/groups',
// summary: 'Create team group',
// description: 'Create a new group for a team',
// tags: ['Team'],
// },
// };
export const ZCreateTeamGroupsRequestSchema = z.object({
teamId: z.number(),
groups: z.array(
z.object({
teamRole: z.nativeEnum(TeamMemberRole).describe('The team role to assign to the group'),
organisationGroupId: z
.string()
.describe(
'The ID of the organisation group to create the team group from. Members from the organisation group will be assigned automatically to this team group.',
),
}),
),
});
export const ZCreateTeamGroupsResponseSchema = z.void();

View File

@ -0,0 +1,159 @@
import { OrganisationGroupType, TeamMemberRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateTeamMembersRequestSchema,
ZCreateTeamMembersResponseSchema,
} from './create-team-members.types';
export const createTeamMembersRoute = authenticatedProcedure
.input(ZCreateTeamMembersRequestSchema)
.output(ZCreateTeamMembersResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, organisationMembers } = input;
const { user } = ctx;
return await createTeamMembers({
userId: user.id,
teamId,
membersToCreate: organisationMembers,
});
});
type CreateTeamMembersOptions = {
userId: number;
teamId: number;
membersToCreate: {
organisationMemberId: string;
teamRole: TeamMemberRole;
}[];
};
export const createTeamMembers = async ({
userId,
teamId,
membersToCreate,
}: CreateTeamMembersOptions) => {
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
include: {
organisation: {
include: {
members: {
select: {
id: true,
},
},
},
},
teamGroups: {
where: {
organisationGroup: {
type: OrganisationGroupType.INTERNAL_TEAM,
},
},
include: {
organisationGroup: true,
},
},
},
});
if (!team) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team not found or missing permissions',
});
}
const isMembersPartOfOrganisation = membersToCreate.every((member) =>
team.organisation.members.some(({ id }) => id === member.organisationMemberId),
);
if (!isMembersPartOfOrganisation) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Some member IDs do not exist',
});
}
const teamMemberGroup = team.teamGroups.find(
(group) =>
group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM &&
group.teamId === teamId &&
group.teamRole === TeamMemberRole.MEMBER,
);
const teamManagerGroup = team.teamGroups.find(
(group) =>
group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM &&
group.teamId === teamId &&
group.teamRole === TeamMemberRole.MANAGER,
);
const teamAdminGroup = team.teamGroups.find(
(group) =>
group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM &&
group.teamId === teamId &&
group.teamRole === TeamMemberRole.ADMIN,
);
if (!teamMemberGroup || !teamManagerGroup || !teamAdminGroup) {
console.error({
message: 'Team groups not found.',
teamMemberGroup: Boolean(teamMemberGroup),
teamManagerGroup: Boolean(teamManagerGroup),
teamAdminGroup: Boolean(teamAdminGroup),
});
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team groups not found.',
});
}
const { teamRole: currentUserTeamRole } = await getMemberRoles({
teamId,
reference: {
type: 'User',
id: userId,
},
});
if (
!membersToCreate.every((member) =>
isTeamRoleWithinUserHierarchy(currentUserTeamRole, member.teamRole),
)
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Cannot add a member with a role higher than your own',
});
}
// const currentUserRole = team.teamGroups.
console.log({
test: membersToCreate.map((member) => ({
organisationMemberId: member.organisationMemberId,
groupId: match(member.teamRole)
.with(TeamMemberRole.MEMBER, () => teamMemberGroup.id)
.with(TeamMemberRole.MANAGER, () => teamManagerGroup.id)
.with(TeamMemberRole.ADMIN, () => teamAdminGroup.id)
.exhaustive(),
})),
});
await prisma.organisationGroupMember.createMany({
data: membersToCreate.map((member) => ({
organisationMemberId: member.organisationMemberId,
groupId: match(member.teamRole)
.with(TeamMemberRole.MEMBER, () => teamMemberGroup.organisationGroupId)
.with(TeamMemberRole.MANAGER, () => teamManagerGroup.organisationGroupId)
.with(TeamMemberRole.ADMIN, () => teamAdminGroup.organisationGroupId)
.exhaustive(),
})),
});
};

View File

@ -0,0 +1,40 @@
import { z } from 'zod';
import { TeamMemberRole } from '@documenso/prisma/generated/types';
export const ZCreateTeamMembersRequestSchema = z.object({
teamId: z.number(),
organisationMembers: z
.array(
z.object({
organisationMemberId: z.string(),
teamRole: z.nativeEnum(TeamMemberRole).describe('The team role to add the user as'),
}),
)
.min(1)
// Todo: orgs test
.superRefine((items, ctx) => {
const uniqueIds = new Map<string, number>();
for (const [index, organisationMember] of items.entries()) {
const email = organisationMember.organisationMemberId;
const firstFoundIndex = uniqueIds.get(email);
if (firstFoundIndex === undefined) {
uniqueIds.set(email, index);
continue;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'IDs must be unique',
path: ['organisationMembers', index, 'organisationMemberId'],
});
}
}),
});
export const ZCreateTeamMembersResponseSchema = z.void();
export type TCreateTeamMembersRequestSchema = z.infer<typeof ZCreateTeamMembersRequestSchema>;

View File

@ -0,0 +1,21 @@
import { createTeam } from '@documenso/lib/server-only/team/create-team';
import { authenticatedProcedure } from '../trpc';
import { ZCreateTeamRequestSchema, ZCreateTeamResponseSchema } from './create-team.types';
export const createTeamRoute = authenticatedProcedure
// .meta(createOrganisationGroupMeta)
.input(ZCreateTeamRequestSchema)
.output(ZCreateTeamResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamName, teamUrl, organisationId, inheritMembers } = input;
const { user } = ctx;
return await createTeam({
userId: user.id,
teamName,
teamUrl,
organisationId,
inheritMembers,
});
});

View File

@ -0,0 +1,38 @@
import { z } from 'zod';
import { ZTeamUrlSchema } from './schema';
import { ZTeamNameSchema } from './schema';
// export const createTeamMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/team/create',
// summary: 'Create team',
// description: 'Create a new team',
// tags: ['Team'],
// },
// };
export const ZCreateTeamRequestSchema = z.object({
organisationId: z.string(),
teamName: ZTeamNameSchema,
teamUrl: ZTeamUrlSchema,
inheritMembers: z
.boolean()
.describe(
'Whether to automatically assign all current and future organisation members to the new team. Defaults to true.',
),
});
export const ZCreateTeamResponseSchema = z.union([
z.object({
paymentRequired: z.literal(false),
}),
z.object({
paymentRequired: z.literal(true),
pendingTeamId: z.number(),
}),
]);
export type TCreateTeamRequest = z.infer<typeof ZCreateTeamRequestSchema>;
export type TCreateTeamResponse = z.infer<typeof ZCreateTeamResponseSchema>;

View File

@ -0,0 +1,80 @@
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { OrganisationGroupType, OrganisationMemberRole } from '@documenso/prisma/generated/types';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteTeamGroupRequestSchema,
ZDeleteTeamGroupResponseSchema,
} from './delete-team-group.types';
export const deleteTeamGroupRoute = authenticatedProcedure
// .meta(deleteTeamGroupMeta)
.input(ZDeleteTeamGroupRequestSchema)
.output(ZDeleteTeamGroupResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamGroupId, teamId } = input;
const { user } = ctx;
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery(teamId, user.id, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
});
if (!team) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
const group = await prisma.teamGroup.findFirst({
where: {
id: teamGroupId,
team: {
id: teamId,
},
},
include: {
organisationGroup: true,
},
});
if (!group) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team group not found',
});
}
// You cannot delete internal organisation groups.
// The only exception is deleting the "member" organisation group which is used to allow
// all organisation members to access a team.
if (
group.organisationGroup.type === OrganisationGroupType.INTERNAL_ORGANISATION &&
group.organisationGroup.organisationRole !== OrganisationMemberRole.MEMBER
) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not allowed to delete internal organisaion groups',
});
}
const { teamRole: currentUserTeamRole } = await getMemberRoles({
teamId,
reference: {
type: 'User',
id: user.id,
},
});
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, group.teamRole)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not allowed to delete this team group',
});
}
await prisma.teamGroup.delete({
where: {
id: teamGroupId,
teamId,
},
});
});

View File

@ -0,0 +1,18 @@
import { z } from 'zod';
// export const deleteTeamGroupMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/team/groups/{id}/delete',
// summary: 'Delete team group',
// description: 'Delete an existing group for a team',
// tags: ['Team'],
// },
// };
export const ZDeleteTeamGroupRequestSchema = z.object({
teamId: z.number(),
teamGroupId: z.string(),
});
export const ZDeleteTeamGroupResponseSchema = z.void();

View File

@ -0,0 +1,103 @@
import { OrganisationGroupType } from '@prisma/client';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteTeamMemberRequestSchema,
ZDeleteTeamMemberResponseSchema,
} from './delete-team-member.types';
export const deleteTeamMemberRoute = authenticatedProcedure
// .meta(deleteTeamMemberMeta)
.input(ZDeleteTeamMemberRequestSchema)
.output(ZDeleteTeamMemberResponseSchema)
.mutation(async ({ ctx, input }) => {
const { teamId, memberId } = input;
const { user } = ctx;
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery(teamId, user.id, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
include: {
teamGroups: {
where: {
organisationGroup: {
type: OrganisationGroupType.INTERNAL_TEAM,
organisationGroupMembers: {
some: {
organisationMember: {
id: memberId,
},
},
},
},
},
include: {
organisationGroup: {
include: {
organisationGroupMembers: {
include: {
organisationMember: true,
},
},
},
},
},
},
},
});
if (!team) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
const { teamRole: currentUserTeamRole } = await getMemberRoles({
teamId,
reference: {
type: 'User',
id: user.id,
},
});
const { teamRole: currentMemberToDeleteTeamRole } = await getMemberRoles({
teamId,
reference: {
type: 'Member',
id: memberId,
},
});
// Check role permissions.
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, currentMemberToDeleteTeamRole)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Cannot remove a member with a higher role',
});
}
const teamGroupToRemoveMemberFrom = team.teamGroups[0];
// Sanity check.
if (team.teamGroups.length !== 1) {
console.error('Team has more than one internal team group. This should not happen.');
// Todo: Logging.
}
if (team.teamGroups.length === 0) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Team has no internal team groups',
});
}
await prisma.organisationGroupMember.delete({
where: {
organisationMemberId_groupId: {
organisationMemberId: memberId,
groupId: teamGroupToRemoveMemberFrom.organisationGroupId,
},
},
});
});

View File

@ -0,0 +1,18 @@
import { z } from 'zod';
// export const deleteTeamMemberMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/team/member/delete',
// summary: 'Delete team member',
// description: 'Delete team member',
// tags: ['Team'],
// },
// };
export const ZDeleteTeamMemberRequestSchema = z.object({
teamId: z.number().describe('The ID of the team to remove the member from.'),
memberId: z.string().describe('The ID of the member to remove from the team.'),
});
export const ZDeleteTeamMemberResponseSchema = z.void();

View File

@ -0,0 +1,18 @@
import { deleteTeam } from '@documenso/lib/server-only/team/delete-team';
import { authenticatedProcedure } from '../trpc';
import { ZDeleteTeamRequestSchema, ZDeleteTeamResponseSchema } from './delete-team.types';
export const deleteTeamRoute = authenticatedProcedure
// .meta(deleteTeamMeta)
.input(ZDeleteTeamRequestSchema)
.output(ZDeleteTeamResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = input;
const { user } = ctx;
await deleteTeam({
userId: user.id,
teamId,
});
});

View File

@ -0,0 +1,17 @@
import { z } from 'zod';
// export const deleteTeamMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'DELETE',
// path: '/team/{teamId}',
// summary: 'Delete team',
// description: 'Delete an existing team',
// tags: ['Team'],
// },
// };
export const ZDeleteTeamRequestSchema = z.object({
teamId: z.number(),
});
export const ZDeleteTeamResponseSchema = z.void();

View File

@ -0,0 +1,144 @@
import type { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { unique } from 'remeda';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZFindTeamGroupsRequestSchema,
ZFindTeamGroupsResponseSchema,
} from './find-team-groups.types';
export const findTeamGroupsRoute = authenticatedProcedure
// .meta(getTeamGroupsMeta)
.input(ZFindTeamGroupsRequestSchema)
.output(ZFindTeamGroupsResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, types, query, page, perPage, teamGroupId, organisationRoles } = input;
const { user } = ctx;
return await findTeamGroups({
userId: user.id,
teamId,
teamGroupId,
types: unique(types || []),
organisationRoles: unique(organisationRoles || []),
query,
page,
perPage,
});
});
type FindTeamGroupsOptions = {
userId: number;
teamId: number;
teamGroupId?: string;
types?: OrganisationGroupType[];
organisationRoles?: OrganisationMemberRole[];
query?: string;
page?: number;
perPage?: number;
};
export const findTeamGroups = async ({
userId,
teamId,
teamGroupId,
types = [],
organisationRoles = [],
query,
page = 1,
perPage = 10,
}: FindTeamGroupsOptions) => {
const whereClause: Prisma.TeamGroupWhereInput = {
team: buildTeamWhereQuery(teamId, userId),
id: teamGroupId,
organisationGroup: {
organisationRole: organisationRoles.length > 0 ? { in: organisationRoles } : undefined,
type:
types.length > 0
? {
in: types,
}
: undefined,
...(query && {
name: {
contains: query,
mode: Prisma.QueryMode.insensitive,
},
}),
},
};
const [data, count] = await Promise.all([
prisma.teamGroup.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
organisationGroup: {
name: 'desc',
},
},
select: {
id: true,
teamRole: true,
teamId: true,
organisationGroup: {
select: {
id: true,
name: true,
type: true,
organisationGroupMembers: {
select: {
organisationMember: {
select: {
id: true,
user: {
select: {
id: true,
name: true,
email: true,
avatarImageId: true,
},
},
},
},
},
},
},
},
},
}),
prisma.teamGroup.count({
where: whereClause,
}),
]);
const mappedData = data.map((group) => ({
id: group.id,
teamId: group.teamId,
teamRole: group.teamRole,
name: group.organisationGroup.name || '',
organisationGroupId: group.organisationGroup.id,
organisationGroupType: group.organisationGroup.type,
members: group.organisationGroup.organisationGroupMembers.map(({ organisationMember }) => ({
id: organisationMember.id,
userId: organisationMember.user.id,
name: organisationMember.user.name || '',
email: organisationMember.user.email,
avatarImageId: organisationMember.user.avatarImageId,
})),
}));
return {
data: mappedData,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof mappedData>;
};

View File

@ -0,0 +1,47 @@
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
import { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { TeamGroupSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamGroupSchema';
// export const getTeamGroupsMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'GET',
// path: '/team/{teamId}/groups',
// summary: 'Get team groups',
// description: 'Get all groups for a team',
// tags: ['Team'],
// },
// };
export const ZFindTeamGroupsRequestSchema = ZFindSearchParamsSchema.extend({
teamId: z.number(),
teamGroupId: z.string().optional(),
organisationRoles: z.nativeEnum(OrganisationMemberRole).array().optional(),
types: z.nativeEnum(OrganisationGroupType).array().optional(),
});
export const ZFindTeamGroupsResponseSchema = ZFindResultResponse.extend({
data: TeamGroupSchema.pick({
teamRole: true,
id: true,
teamId: true,
})
.extend({
name: z.string(),
organisationGroupId: z.string(),
organisationGroupType: z.nativeEnum(OrganisationGroupType),
members: z
.object({
id: z.string(),
userId: z.number(),
name: z.string(),
email: z.string(),
avatarImageId: z.string().nullable(),
})
.array(),
})
.array(),
});
export type TFindTeamGroupsResponse = z.infer<typeof ZFindTeamGroupsResponseSchema>;

View File

@ -0,0 +1,23 @@
import { findTeamMembers } from '@documenso/lib/server-only/team/find-team-members';
import { authenticatedProcedure } from '../trpc';
import {
ZFindTeamMembersRequestSchema,
ZFindTeamMembersResponseSchema,
} from './find-team-members.types';
export const findTeamMembersRoute = authenticatedProcedure
.input(ZFindTeamMembersRequestSchema)
.output(ZFindTeamMembersResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, query, page, perPage } = input;
const { user } = ctx;
return await findTeamMembers({
userId: user.id,
teamId,
query,
page,
perPage,
});
});

View File

@ -0,0 +1,27 @@
import { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { OrganisationMemberRole, TeamMemberRole } from '@documenso/prisma/generated/types';
import OrganisationMemberSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberSchema';
export const ZFindTeamMembersRequestSchema = ZFindSearchParamsSchema.extend({
teamId: z.number(),
});
export const ZFindTeamMembersResponseSchema = ZFindResultResponse.extend({
data: OrganisationMemberSchema.pick({
id: true,
createdAt: true,
userId: true,
})
.extend({
teamRole: z.nativeEnum(TeamMemberRole),
organisationRole: z.nativeEnum(OrganisationMemberRole),
email: z.string(),
name: z.string().nullable(),
avatarImageId: z.string().nullable(),
})
.array(),
});
export type TFindTeamMembersResponse = z.infer<typeof ZFindTeamMembersResponseSchema>;

View File

@ -0,0 +1,15 @@
import { findTeams } from '@documenso/lib/server-only/team/find-teams';
import { authenticatedProcedure } from '../trpc';
import { ZFindTeamsRequestSchema, ZFindTeamsResponseSchema } from './find-teams.types';
export const findTeamsRoute = authenticatedProcedure
// .meta(getTeamsMeta)
.input(ZFindTeamsRequestSchema)
.output(ZFindTeamsResponseSchema)
.query(async ({ ctx, input }) => {
const { organisationId } = input;
const { user } = ctx;
return findTeams({ userId: user.id, organisationId });
});

View File

@ -0,0 +1,39 @@
import { TeamMemberRole } from '@prisma/client';
import { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
// export const getTeamsMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'GET',
// path: '/team/teams',
// summary: 'Get teams',
// description: 'Get all teams you are a member of',
// tags: ['team'],
// },
// };
export const ZFindTeamsRequestSchema = ZFindSearchParamsSchema.extend({
organisationId: z.string(),
});
export const ZFindTeamsResponseSchema = z.any();
// Todo: orgs
export const ZFindTeamsResponseSchemaZZZ = ZFindResultResponse.extend({
data: TeamSchema.pick({
id: true,
name: true,
url: true,
createdAt: true,
avatarImageId: true,
organisationId: true,
})
.extend({
teamRole: z.nativeEnum(TeamMemberRole),
})
.array(),
});
export type TFindTeamsResponse = z.infer<typeof ZFindTeamsResponseSchema>;

View File

@ -0,0 +1,21 @@
import { getTeamMembers } from '@documenso/lib/server-only/team/get-team-members';
import { authenticatedProcedure } from '../trpc';
import {
ZGetTeamMembersRequestSchema,
ZGetTeamMembersResponseSchema,
} from './get-team-members.types';
export const getTeamMembersRoute = authenticatedProcedure
// .meta(getTeamMembersMeta)
.input(ZGetTeamMembersRequestSchema)
.output(ZGetTeamMembersResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = input;
const { user } = ctx;
return await getTeamMembers({
userId: user.id,
teamId,
});
});

View File

@ -0,0 +1,34 @@
import { z } from 'zod';
import { OrganisationMemberRole, TeamMemberRole } from '@documenso/prisma/generated/types';
import OrganisationMemberSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberSchema';
// export const getTeamMembersMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'GET',
// path: '/team/{teamId}/members',
// summary: 'Get team members',
// description: 'Get all members of a team',
// tags: ['team'],
// },
// };
export const ZGetTeamMembersRequestSchema = z.object({
teamId: z.number(),
});
export const ZGetTeamMembersResponseSchema = OrganisationMemberSchema.pick({
id: true,
createdAt: true,
userId: true,
})
.extend({
teamRole: z.nativeEnum(TeamMemberRole),
organisationRole: z.nativeEnum(OrganisationMemberRole),
email: z.string(),
name: z.string().nullable(),
avatarImageId: z.string().nullable(),
})
.array();
export type TGetTeamMembersResponse = z.infer<typeof ZGetTeamMembersResponseSchema>;

View File

@ -0,0 +1,79 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import {
buildTeamWhereQuery,
extractDerivedTeamSettings,
getHighestTeamRoleInGroup,
} from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import { ZGetTeamRequestSchema, ZGetTeamResponseSchema } from './get-team.types';
export const getTeamRoute = authenticatedProcedure
// .meta(getTeamMeta)
.input(ZGetTeamRequestSchema)
.output(ZGetTeamResponseSchema)
.query(async ({ input, ctx }) => {
return await getTeam({
teamReference: input.teamReference,
userId: ctx.user.id,
});
});
/**
* Get a team by its ID or URL.
*
* Todo: orgs there's multiple implementations of this.
*/
export const getTeam = async ({
teamReference,
userId,
}: {
teamReference: number | string;
userId: number;
}) => {
const team = await prisma.team.findFirst({
where: {
id: typeof teamReference === 'number' ? teamReference : undefined,
url: typeof teamReference === 'string' ? teamReference : undefined,
...buildTeamWhereQuery(undefined, userId),
},
include: {
teamGroups: {
where: {
organisationGroup: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
},
teamGlobalSettings: true,
organisation: {
include: {
organisationGlobalSettings: true,
},
},
},
});
if (!team) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team not found',
});
}
const organisationSettings = team.organisation.organisationGlobalSettings;
const teamSettings = team.teamGlobalSettings;
return {
...team,
currentTeamRole: getHighestTeamRoleInGroup(team.teamGroups),
teamSettings,
derivedSettings: extractDerivedTeamSettings(organisationSettings, teamSettings),
};
};

View File

@ -0,0 +1,39 @@
import { z } from 'zod';
import { TeamMemberRole } from '@documenso/prisma/generated/types';
import OrganisationGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationGlobalSettingsSchema';
import TeamGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/TeamGlobalSettingsSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
// export const getTeamMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'GET',
// path: '/team/{teamReference}',
// summary: 'Get team',
// description: 'Get a team by ID or URL',
// tags: ['team'],
// },
// };
export const ZGetTeamRequestSchema = z.object({
teamReference: z.union([z.string(), z.number()]),
});
export const ZGetTeamResponseSchema = TeamSchema.pick({
id: true,
name: true,
url: true,
createdAt: true,
avatarImageId: true,
organisationId: true,
}).extend({
currentTeamRole: z.nativeEnum(TeamMemberRole),
teamSettings: TeamGlobalSettingsSchema.omit({
id: true,
}),
derivedSettings: OrganisationGlobalSettingsSchema.omit({
id: true,
}),
});
export type TGetTeamResponse = z.infer<typeof ZGetTeamResponseSchema>;

View File

@ -1,338 +1,70 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { getTeamPrices } from '@documenso/ee/server-only/stripe/get-team-prices';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { acceptTeamInvitation } from '@documenso/lib/server-only/team/accept-team-invitation';
import { createTeam } from '@documenso/lib/server-only/team/create-team';
import { createTeamBillingPortal } from '@documenso/lib/server-only/team/create-team-billing-portal';
import { createTeamPendingCheckoutSession } from '@documenso/lib/server-only/team/create-team-checkout-session';
import { createTeamEmailVerification } from '@documenso/lib/server-only/team/create-team-email-verification';
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
import { declineTeamInvitation } from '@documenso/lib/server-only/team/decline-team-invitation';
import { deleteTeam } from '@documenso/lib/server-only/team/delete-team';
import { deleteTeamEmail } from '@documenso/lib/server-only/team/delete-team-email';
import { deleteTeamEmailVerification } from '@documenso/lib/server-only/team/delete-team-email-verification';
import { deleteTeamMemberInvitations } from '@documenso/lib/server-only/team/delete-team-invitations';
import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members';
import { deleteTeamPending } from '@documenso/lib/server-only/team/delete-team-pending';
import { deleteTeamTransferRequest } from '@documenso/lib/server-only/team/delete-team-transfer-request';
import { findTeamInvoices } from '@documenso/lib/server-only/team/find-team-invoices';
import { findTeamMemberInvites } from '@documenso/lib/server-only/team/find-team-member-invites';
import { findTeamMembers } from '@documenso/lib/server-only/team/find-team-members';
import { findTeams } from '@documenso/lib/server-only/team/find-teams';
import { findTeamsPending } from '@documenso/lib/server-only/team/find-teams-pending';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { getTeamEmailByEmail } from '@documenso/lib/server-only/team/get-team-email-by-email';
import { getTeamInvitations } from '@documenso/lib/server-only/team/get-team-invitations';
import { getTeamMembers } from '@documenso/lib/server-only/team/get-team-members';
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
import { leaveTeam } from '@documenso/lib/server-only/team/leave-team';
import { requestTeamOwnershipTransfer } from '@documenso/lib/server-only/team/request-team-ownership-transfer';
import { resendTeamEmailVerification } from '@documenso/lib/server-only/team/resend-team-email-verification';
import { resendTeamMemberInvitation } from '@documenso/lib/server-only/team/resend-team-member-invitation';
import { updateTeam } from '@documenso/lib/server-only/team/update-team';
import { updateTeamBrandingSettings } from '@documenso/lib/server-only/team/update-team-branding-settings';
import { updateTeamEmail } from '@documenso/lib/server-only/team/update-team-email';
import { updateTeamMember } from '@documenso/lib/server-only/team/update-team-member';
import { updateTeamPublicProfile } from '@documenso/lib/server-only/team/update-team-public-profile';
import { authenticatedProcedure, router } from '../trpc';
import { createTeamRoute } from './create-team';
import { createTeamGroupsRoute } from './create-team-groups';
import { createTeamMembersRoute } from './create-team-members';
import { deleteTeamRoute } from './delete-team';
import { deleteTeamGroupRoute } from './delete-team-group';
import { deleteTeamMemberRoute } from './delete-team-member';
import { findTeamGroupsRoute } from './find-team-groups';
import { findTeamMembersRoute } from './find-team-members';
import { findTeamsRoute } from './find-teams';
import { getTeamRoute } from './get-team';
import { getTeamMembersRoute } from './get-team-members';
import {
ZAcceptTeamInvitationMutationSchema,
ZCreateTeamBillingPortalMutationSchema,
ZCreateTeamEmailVerificationMutationSchema,
ZCreateTeamMemberInvitesMutationSchema,
ZCreateTeamMutationSchema,
ZCreateTeamPendingCheckoutMutationSchema,
ZDeclineTeamInvitationMutationSchema,
ZDeleteTeamEmailMutationSchema,
ZDeleteTeamEmailVerificationMutationSchema,
ZDeleteTeamMemberInvitationsMutationSchema,
ZDeleteTeamMembersMutationSchema,
ZDeleteTeamMutationSchema,
ZDeleteTeamPendingMutationSchema,
ZDeleteTeamTransferRequestMutationSchema,
ZFindTeamInvoicesQuerySchema,
ZFindTeamMemberInvitesQuerySchema,
ZFindTeamMembersQuerySchema,
ZFindTeamsPendingQuerySchema,
ZFindTeamsQuerySchema,
ZGetTeamMembersQuerySchema,
ZGetTeamQuerySchema,
ZLeaveTeamMutationSchema,
ZRequestTeamOwnerhsipTransferMutationSchema,
ZResendTeamEmailVerificationMutationSchema,
ZResendTeamMemberInvitationMutationSchema,
ZUpdateTeamBrandingSettingsMutationSchema,
ZUpdateTeamEmailMutationSchema,
ZUpdateTeamMemberMutationSchema,
ZUpdateTeamMutationSchema,
ZUpdateTeamPublicProfileMutationSchema,
} from './schema';
import { updateTeamDocumentSettingsRoute } from './update-team-document-settings';
import { updateTeamRoute } from './update-team';
import { updateTeamGroupRoute } from './update-team-group';
import { updateTeamMemberRoute } from './update-team-member';
import { updateTeamSettingsRoute } from './update-team-settings';
export const teamRouter = router({
// Internal endpoint for now.
getTeams: authenticatedProcedure.query(async ({ ctx }) => {
return await getTeams({ userId: ctx.user.id });
}),
find: findTeamsRoute,
get: getTeamRoute,
create: createTeamRoute,
update: updateTeamRoute,
delete: deleteTeamRoute,
member: {
find: findTeamMembersRoute,
getMany: getTeamMembersRoute,
createMany: createTeamMembersRoute,
update: updateTeamMemberRoute,
delete: deleteTeamMemberRoute,
},
group: {
find: findTeamGroupsRoute,
createMany: createTeamGroupsRoute,
update: updateTeamGroupRoute,
delete: deleteTeamGroupRoute,
},
settings: {
update: updateTeamSettingsRoute,
},
// Todo: Public endpoint.
findTeams: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team',
// summary: 'Find teams',
// description: 'Find your teams based on a search criteria',
// tags: ['Teams'],
// },
// })
.input(ZFindTeamsQuerySchema)
.query(async ({ input, ctx }) => {
return await findTeams({
userId: ctx.user.id,
...input,
});
}),
// Todo: Public endpoint.
getTeam: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team/{teamId}',
// summary: 'Get team',
// tags: ['Teams'],
// },
// })
.input(ZGetTeamQuerySchema)
.query(async ({ input, ctx }) => {
return await getTeamById({ teamId: input.teamId, userId: ctx.user.id });
}),
// Todo: Public endpoint.
createTeam: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/create',
// summary: 'Create team',
// tags: ['Teams'],
// },
// })
.input(ZCreateTeamMutationSchema)
.mutation(async ({ input, ctx }) => {
return await createTeam({
userId: ctx.user.id,
...input,
});
}),
// Todo: Public endpoint.
updateTeam: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}',
// summary: 'Update team',
// tags: ['Teams'],
// },
// })
.input(ZUpdateTeamMutationSchema)
.mutation(async ({ input, ctx }) => {
return await updateTeam({
userId: ctx.user.id,
...input,
});
}),
// Todo: Public endpoint.
deleteTeam: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/delete',
// summary: 'Delete team',
// tags: ['Teams'],
// },
// })
.input(ZDeleteTeamMutationSchema)
.mutation(async ({ input, ctx }) => {
return await deleteTeam({
userId: ctx.user.id,
...input,
});
}),
// Todo: Public endpoint.
leaveTeam: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/leave',
// summary: 'Leave a team',
// description: '',
// tags: ['Teams'],
// },
// })
.input(ZLeaveTeamMutationSchema)
.mutation(async ({ input, ctx }) => {
return await leaveTeam({
userId: ctx.user.id,
...input,
});
}),
// Todo: Public endpoint.
findTeamMemberInvites: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team/{teamId}/member/invite',
// summary: 'Find member invites',
// description: 'Returns pending team member invites',
// tags: ['Teams'],
// },
// })
.input(ZFindTeamMemberInvitesQuerySchema)
.query(async ({ input, ctx }) => {
return await findTeamMemberInvites({
userId: ctx.user.id,
...input,
});
}),
// Todo: Public endpoint.
createTeamMemberInvites: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/member/invite',
// summary: 'Invite members',
// description: 'Send email invitations to users to join the team',
// tags: ['Teams'],
// },
// })
.input(ZCreateTeamMemberInvitesMutationSchema)
.mutation(async ({ input, ctx }) => {
return await createTeamMemberInvites({
userId: ctx.user.id,
userName: ctx.user.name ?? '',
...input,
});
}),
// Todo: Public endpoint.
resendTeamMemberInvitation: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/member/invite/{invitationId}/resend',
// summary: 'Resend member invite',
// description: 'Resend an email invitation to a user to join the team',
// tags: ['Teams'],
// },
// })
.input(ZResendTeamMemberInvitationMutationSchema)
.mutation(async ({ input, ctx }) => {
await resendTeamMemberInvitation({
userId: ctx.user.id,
userName: ctx.user.name ?? '',
...input,
});
}),
// Todo: Public endpoint.
deleteTeamMemberInvitations: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/member/invite/delete',
// summary: 'Delete member invite',
// description: 'Delete a pending team member invite',
// tags: ['Teams'],
// },
// })
.input(ZDeleteTeamMemberInvitationsMutationSchema)
.mutation(async ({ input, ctx }) => {
return await deleteTeamMemberInvitations({
userId: ctx.user.id,
...input,
});
}),
// Todo: Public endpoint.
getTeamMembers: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team/{teamId}/member',
// summary: 'Get members',
// tags: ['Teams'],
// },
// })
.input(ZGetTeamMembersQuerySchema)
.query(async ({ input, ctx }) => {
return await getTeamMembers({ teamId: input.teamId, userId: ctx.user.id });
}),
// Todo: Public endpoint.
findTeamMembers: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team/{teamId}/member/find',
// summary: 'Find members',
// description: 'Find team members based on a search criteria',
// tags: ['Teams'],
// },
// })
.input(ZFindTeamMembersQuerySchema)
.query(async ({ input, ctx }) => {
return await findTeamMembers({
userId: ctx.user.id,
...input,
});
}),
// Todo: Public endpoint.
updateTeamMember: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/member/{teamMemberId}',
// summary: 'Update member',
// tags: ['Teams'],
// },
// })
.input(ZUpdateTeamMemberMutationSchema)
.mutation(async ({ input, ctx }) => {
return await updateTeamMember({
userId: ctx.user.id,
...input,
});
}),
// Todo: Public endpoint.
deleteTeamMembers: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/member/delete',
// summary: 'Delete members',
// description: '',
// tags: ['Teams'],
// },
// })
.input(ZDeleteTeamMembersMutationSchema)
.mutation(async ({ input, ctx }) => {
return await deleteTeamMembers({
userId: ctx.user.id,
...input,
});
}),
// Old routes (to be migrated)
// Internal endpoint for now.
createTeamEmailVerification: authenticatedProcedure
@ -357,22 +89,6 @@ export const teamRouter = router({
});
}),
// Internal endpoint for now.
getTeamInvitations: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team/invite',
// summary: 'Get team invitations',
// description: '',
// tags: ['Teams'],
// },
// })
.input(z.void())
.query(async ({ ctx }) => {
return await getTeamInvitations({ email: ctx.user.email });
}),
// Todo: Public endpoint.
updateTeamPublicProfile: authenticatedProcedure
// .meta({
@ -414,44 +130,6 @@ export const teamRouter = router({
}
}),
// Internal endpoint for now.
requestTeamOwnershipTransfer: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/transfer',
// summary: 'Request a team ownership transfer',
// description: '',
// tags: ['Teams'],
// },
// })
.input(ZRequestTeamOwnerhsipTransferMutationSchema)
.mutation(async ({ input, ctx }) => {
return await requestTeamOwnershipTransfer({
userId: ctx.user.id,
userName: ctx.user.name ?? '',
...input,
});
}),
// Internal endpoint for now.
deleteTeamTransferRequest: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/transfer/delete',
// summary: 'Delete team transfer request',
// tags: ['Teams'],
// },
// })
.input(ZDeleteTeamTransferRequestMutationSchema)
.mutation(async ({ input, ctx }) => {
return await deleteTeamTransferRequest({
userId: ctx.user.id,
...input,
});
}),
// Todo
getTeamEmailByEmail: authenticatedProcedure.query(async ({ ctx }) => {
return await getTeamEmailByEmail({ email: ctx.user.email });
@ -532,19 +210,6 @@ export const teamRouter = router({
});
}),
// Internal endpoint. Use updateTeam instead.
updateTeamBrandingSettings: authenticatedProcedure
.input(ZUpdateTeamBrandingSettingsMutationSchema)
.mutation(async ({ ctx, input }) => {
const { teamId, settings } = input;
return await updateTeamBrandingSettings({
userId: ctx.user.id,
teamId,
settings,
});
}),
// Internal endpoint for now.
createTeamPendingCheckout: authenticatedProcedure
.input(ZCreateTeamPendingCheckoutMutationSchema)
@ -555,53 +220,11 @@ export const teamRouter = router({
});
}),
// Internal endpoint for now.
findTeamInvoices: authenticatedProcedure
.input(ZFindTeamInvoicesQuerySchema)
.query(async ({ input, ctx }) => {
return await findTeamInvoices({
userId: ctx.user.id,
...input,
});
}),
// Internal endpoint for now.
getTeamPrices: authenticatedProcedure.query(async () => {
return await getTeamPrices();
}),
updateTeamDocumentSettings: updateTeamDocumentSettingsRoute,
// Internal endpoint for now.
acceptTeamInvitation: authenticatedProcedure
.input(ZAcceptTeamInvitationMutationSchema)
.mutation(async ({ input, ctx }) => {
return await acceptTeamInvitation({
teamId: input.teamId,
userId: ctx.user.id,
});
}),
// Internal endpoint for now.
declineTeamInvitation: authenticatedProcedure
.input(ZDeclineTeamInvitationMutationSchema)
.mutation(async ({ input, ctx }) => {
return await declineTeamInvitation({
teamId: input.teamId,
userId: ctx.user.id,
});
}),
// Internal endpoint for now.
createBillingPortal: authenticatedProcedure
.input(ZCreateTeamBillingPortalMutationSchema)
.mutation(async ({ input, ctx }) => {
return await createTeamBillingPortal({
userId: ctx.user.id,
...input,
});
}),
// Internal endpoint for now.
findTeamsPending: authenticatedProcedure
// .meta({

View File

@ -4,7 +4,7 @@ import { z } from 'zod';
import { PROTECTED_TEAM_URLS } from '@documenso/lib/constants/teams';
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZUpdatePublicProfileMutationSchema } from '../profile-router/schema';
export const MAX_PROFILE_BIO_LENGTH = 256;
/**
* Restrict team URLs schema.
@ -43,39 +43,12 @@ export const ZTeamNameSchema = z
.min(3, { message: 'Team name must be at least 3 characters long.' })
.max(30, { message: 'Team name must not exceed 30 characters.' });
export const ZAcceptTeamInvitationMutationSchema = z.object({
teamId: z.number(),
});
export const ZDeclineTeamInvitationMutationSchema = z.object({
teamId: z.number(),
});
export const ZCreateTeamBillingPortalMutationSchema = z.object({
teamId: z.number(),
});
export const ZCreateTeamMutationSchema = z.object({
teamName: ZTeamNameSchema,
teamUrl: ZTeamUrlSchema,
});
export const ZCreateTeamEmailVerificationMutationSchema = z.object({
teamId: z.number(),
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
email: z.string().trim().email().toLowerCase().min(1, 'Please enter a valid email.'),
});
export const ZCreateTeamMemberInvitesMutationSchema = z.object({
teamId: z.number(),
invitations: z.array(
z.object({
email: z.string().email().toLowerCase(),
role: z.nativeEnum(TeamMemberRole),
}),
),
});
export const ZCreateTeamPendingCheckoutMutationSchema = z.object({
interval: z.union([z.literal('monthly'), z.literal('yearly')]),
pendingTeamId: z.number(),
@ -89,16 +62,6 @@ export const ZDeleteTeamEmailVerificationMutationSchema = z.object({
teamId: z.number(),
});
export const ZDeleteTeamMembersMutationSchema = z.object({
teamId: z.number(),
teamMemberIds: z.array(z.number()),
});
export const ZDeleteTeamMemberInvitationsMutationSchema = z.object({
teamId: z.number(),
invitationIds: z.array(z.number()),
});
export const ZDeleteTeamMutationSchema = z.object({
teamId: z.number(),
});
@ -107,46 +70,12 @@ export const ZDeleteTeamPendingMutationSchema = z.object({
pendingTeamId: z.number(),
});
export const ZDeleteTeamTransferRequestMutationSchema = z.object({
teamId: z.number(),
});
export const ZFindTeamInvoicesQuerySchema = z.object({
teamId: z.number(),
});
export const ZFindTeamMemberInvitesQuerySchema = ZFindSearchParamsSchema.extend({
teamId: z.number(),
});
export const ZFindTeamMembersQuerySchema = ZFindSearchParamsSchema.extend({
teamId: z.number(),
});
export const ZFindTeamsQuerySchema = ZFindSearchParamsSchema;
export const ZFindTeamsPendingQuerySchema = ZFindSearchParamsSchema;
export const ZGetTeamQuerySchema = z.object({
teamId: z.number(),
});
export const ZGetTeamMembersQuerySchema = z.object({
teamId: z.number(),
});
export const ZLeaveTeamMutationSchema = z.object({
teamId: z.number(),
});
export const ZUpdateTeamMutationSchema = z.object({
teamId: z.number(),
data: z.object({
name: ZTeamNameSchema,
url: ZTeamUrlSchema,
}),
});
export const ZUpdateTeamEmailMutationSchema = z.object({
teamId: z.number(),
data: z.object({
@ -162,73 +91,43 @@ export const ZUpdateTeamMemberMutationSchema = z.object({
}),
});
export const ZUpdateTeamPublicProfileMutationSchema = ZUpdatePublicProfileMutationSchema.pick({
bio: true,
enabled: true,
}).extend({
export const ZUpdateTeamPublicProfileMutationSchema = z.object({
bio: z
.string()
.max(MAX_PROFILE_BIO_LENGTH, {
message: `Bio must be shorter than ${MAX_PROFILE_BIO_LENGTH + 1} characters`,
})
.optional(),
enabled: z.boolean().optional(),
url: z
.string()
.trim()
.toLowerCase()
.min(1, { message: 'Please enter a valid username.' })
.regex(/^[a-z0-9-]+$/, {
message: 'Username can only container alphanumeric characters and dashes.',
})
.optional(),
teamId: z.number(),
});
export const ZRequestTeamOwnerhsipTransferMutationSchema = z.object({
teamId: z.number(),
newOwnerUserId: z.number(),
clearPaymentMethods: z.boolean(),
});
export const ZResendTeamEmailVerificationMutationSchema = z.object({
teamId: z.number(),
});
export const ZResendTeamMemberInvitationMutationSchema = z.object({
teamId: z.number(),
invitationId: z.number(),
});
export const ZUpdateTeamBrandingSettingsMutationSchema = z.object({
teamId: z.number(),
settings: z.object({
brandingEnabled: z.boolean().optional().default(false),
brandingLogo: z.string().optional().default(''),
brandingUrl: z.string().optional().default(''),
brandingCompanyDetails: z.string().optional().default(''),
}),
});
export type TCreateTeamMutationSchema = z.infer<typeof ZCreateTeamMutationSchema>;
export type TCreateTeamEmailVerificationMutationSchema = z.infer<
typeof ZCreateTeamEmailVerificationMutationSchema
>;
export type TCreateTeamMemberInvitesMutationSchema = z.infer<
typeof ZCreateTeamMemberInvitesMutationSchema
>;
export type TCreateTeamPendingCheckoutMutationSchema = z.infer<
typeof ZCreateTeamPendingCheckoutMutationSchema
>;
export type TDeleteTeamEmailMutationSchema = z.infer<typeof ZDeleteTeamEmailMutationSchema>;
export type TDeleteTeamMembersMutationSchema = z.infer<typeof ZDeleteTeamMembersMutationSchema>;
export type TDeleteTeamMutationSchema = z.infer<typeof ZDeleteTeamMutationSchema>;
export type TDeleteTeamPendingMutationSchema = z.infer<typeof ZDeleteTeamPendingMutationSchema>;
export type TDeleteTeamTransferRequestMutationSchema = z.infer<
typeof ZDeleteTeamTransferRequestMutationSchema
>;
export type TFindTeamMemberInvitesQuerySchema = z.infer<typeof ZFindTeamMembersQuerySchema>;
export type TFindTeamMembersQuerySchema = z.infer<typeof ZFindTeamMembersQuerySchema>;
export type TFindTeamsQuerySchema = z.infer<typeof ZFindTeamsQuerySchema>;
export type TFindTeamsPendingQuerySchema = z.infer<typeof ZFindTeamsPendingQuerySchema>;
export type TGetTeamQuerySchema = z.infer<typeof ZGetTeamQuerySchema>;
export type TGetTeamMembersQuerySchema = z.infer<typeof ZGetTeamMembersQuerySchema>;
export type TLeaveTeamMutationSchema = z.infer<typeof ZLeaveTeamMutationSchema>;
export type TUpdateTeamMutationSchema = z.infer<typeof ZUpdateTeamMutationSchema>;
export type TUpdateTeamEmailMutationSchema = z.infer<typeof ZUpdateTeamEmailMutationSchema>;
export type TRequestTeamOwnerhsipTransferMutationSchema = z.infer<
typeof ZRequestTeamOwnerhsipTransferMutationSchema
>;
export type TResendTeamEmailVerificationMutationSchema = z.infer<
typeof ZResendTeamEmailVerificationMutationSchema
>;
export type TResendTeamMemberInvitationMutationSchema = z.infer<
typeof ZResendTeamMemberInvitationMutationSchema
>;
export type TUpdateTeamBrandingSettingsMutationSchema = z.infer<
typeof ZUpdateTeamBrandingSettingsMutationSchema
>;

View File

@ -1,71 +0,0 @@
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 { authenticatedProcedure } from '../trpc';
import {
ZUpdateTeamDocumentSettingsRequestSchema,
ZUpdateTeamDocumentSettingsResponseSchema,
} from './update-team-document-settings.types';
/**
* Private route.
*/
export const updateTeamDocumentSettingsRoute = authenticatedProcedure
.input(ZUpdateTeamDocumentSettingsRequestSchema)
.output(ZUpdateTeamDocumentSettingsResponseSchema)
.mutation(async ({ ctx, input }) => {
const { user } = ctx;
const { teamId, settings } = input;
const {
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
} = settings;
const member = await prisma.teamMember.findFirst({
where: {
userId: user.id,
teamId,
role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP.MANAGE_TEAM,
},
},
});
if (!member) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update this team.',
});
}
return await prisma.teamGlobalSettings.upsert({
where: {
teamId,
},
create: {
teamId,
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
},
update: {
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
},
});
});

View File

@ -1,23 +0,0 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import TeamGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/TeamGlobalSettingsSchema';
export const ZUpdateTeamDocumentSettingsRequestSchema = z.object({
teamId: z.number(),
settings: z.object({
documentVisibility: z
.nativeEnum(DocumentVisibility)
.optional()
.default(DocumentVisibility.EVERYONE),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional().default('en'),
includeSenderDetails: z.boolean().optional().default(false),
includeSigningCertificate: z.boolean().optional().default(true),
typedSignatureEnabled: z.boolean().optional().default(true),
uploadSignatureEnabled: z.boolean().optional().default(true),
drawSignatureEnabled: z.boolean().optional().default(true),
}),
});
export const ZUpdateTeamDocumentSettingsResponseSchema = TeamGlobalSettingsSchema;

View File

@ -0,0 +1,76 @@
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { OrganisationGroupType } from '@documenso/prisma/generated/types';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateTeamGroupRequestSchema,
ZUpdateTeamGroupResponseSchema,
} from './update-team-group.types';
export const updateTeamGroupRoute = authenticatedProcedure
// .meta(updateTeamGroupMeta)
.input(ZUpdateTeamGroupRequestSchema)
.output(ZUpdateTeamGroupResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id, data } = input;
const { user } = ctx;
const teamGroup = await prisma.teamGroup.findFirst({
where: {
id,
team: buildTeamWhereQuery(
undefined,
user.id,
TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
),
},
include: {
organisationGroup: true,
},
});
if (!teamGroup) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team group not found',
});
}
if (teamGroup.organisationGroup.type === OrganisationGroupType.INTERNAL_ORGANISATION) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not allowed to update internal organisation groups',
});
}
const { teamRole: currentUserTeamRole } = await getMemberRoles({
teamId: teamGroup.teamId,
reference: {
type: 'User',
id: user.id,
},
});
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, teamGroup.teamRole)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not allowed to update this team group',
});
}
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, data.teamRole)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not allowed to set a team role higher than your own',
});
}
await prisma.teamGroup.update({
where: {
id,
},
data: {
teamRole: data.teamRole,
},
});
});

View File

@ -0,0 +1,24 @@
import { TeamMemberRole } from '@prisma/client';
import { z } from 'zod';
// export const updateTeamGroupMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/team/groups/{id}',
// summary: 'Update team group',
// description: 'Update an existing group for a team',
// tags: ['Team'],
// requiredScopes: ['personal:team:write'],
// },
// };
export const ZUpdateTeamGroupRequestSchema = z.object({
id: z.string(),
data: z.object({
teamRole: z.nativeEnum(TeamMemberRole),
}),
});
export const ZUpdateTeamGroupResponseSchema = z.void();
export type TUpdateTeamGroupRequest = z.infer<typeof ZUpdateTeamGroupRequestSchema>;

View File

@ -0,0 +1,169 @@
import { match } from 'ts-pattern';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { OrganisationGroupType, TeamMemberRole } from '@documenso/prisma/generated/types';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateTeamMemberRequestSchema,
ZUpdateTeamMemberResponseSchema,
} from './update-team-member.types';
export const updateTeamMemberRoute = authenticatedProcedure
// .meta(updateTeamMemberMeta)
.input(ZUpdateTeamMemberRequestSchema)
.output(ZUpdateTeamMemberResponseSchema)
.mutation(async ({ ctx, input }) => {
const { teamId, memberId, data } = input;
const userId = ctx.user.id;
const team = await prisma.team.findFirst({
where: {
AND: [
buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
{
organisation: {
members: {
some: {
id: memberId,
},
},
},
},
],
},
include: {
teamGroups: {
where: {
organisationGroup: {
type: OrganisationGroupType.INTERNAL_TEAM,
},
},
include: {
organisationGroup: {
include: {
organisationGroupMembers: {
include: {
organisationMember: true,
},
},
},
},
},
},
},
});
if (!team) {
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Team not found' });
}
const internalTeamGroupToRemoveMemberFrom = team.teamGroups.find(
(group) =>
group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM &&
group.teamId === teamId &&
group.organisationGroup.organisationGroupMembers.some(
(member) => member.organisationMemberId === memberId,
),
);
if (!internalTeamGroupToRemoveMemberFrom) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Unable to find member role.',
});
}
const teamMemberGroup = team.teamGroups.find(
(group) =>
group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM &&
group.teamId === teamId &&
group.teamRole === TeamMemberRole.MEMBER,
);
const teamManagerGroup = team.teamGroups.find(
(group) =>
group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM &&
group.teamId === teamId &&
group.teamRole === TeamMemberRole.MANAGER,
);
const teamAdminGroup = team.teamGroups.find(
(group) =>
group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM &&
group.teamId === teamId &&
group.teamRole === TeamMemberRole.ADMIN,
);
console.log({
asdf: team.teamGroups,
});
if (!teamMemberGroup || !teamManagerGroup || !teamAdminGroup) {
console.error({
message: 'Team groups not found.',
teamMemberGroup: Boolean(teamMemberGroup),
teamManagerGroup: Boolean(teamManagerGroup),
teamAdminGroup: Boolean(teamAdminGroup),
});
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Team groups not found.',
});
}
const { teamRole: currentUserTeamRole } = await getMemberRoles({
teamId,
reference: {
type: 'User',
id: userId,
},
});
const { teamRole: currentMemberToUpdateTeamRole } = await getMemberRoles({
teamId,
reference: {
type: 'Member',
id: memberId,
},
});
// Check role permissions.
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, currentMemberToUpdateTeamRole)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Cannot update a member with a higher role',
});
}
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, data.role)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Cannot update a member to a role higher than your own',
});
}
// Switch member to new internal team group role.
await prisma.$transaction(async (tx) => {
await tx.organisationGroupMember.delete({
where: {
organisationMemberId_groupId: {
organisationMemberId: memberId,
groupId: internalTeamGroupToRemoveMemberFrom.organisationGroupId,
},
},
});
await tx.organisationGroupMember.create({
data: {
organisationMemberId: memberId,
groupId: match(data.role)
.with(TeamMemberRole.MEMBER, () => teamMemberGroup.organisationGroupId)
.with(TeamMemberRole.MANAGER, () => teamManagerGroup.organisationGroupId)
.with(TeamMemberRole.ADMIN, () => teamAdminGroup.organisationGroupId)
.exhaustive(),
},
});
});
});

View File

@ -0,0 +1,22 @@
import { TeamMemberRole } from '@prisma/client';
import { z } from 'zod';
// export const updateTeamMemberMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/team/member/update',
// summary: 'Update team member',
// description: 'Update team member',
// tags: ['Team'],
// },
// };
export const ZUpdateTeamMemberRequestSchema = z.object({
teamId: z.number(),
memberId: z.string(),
data: z.object({
role: z.nativeEnum(TeamMemberRole),
}),
});
export const ZUpdateTeamMemberResponseSchema = z.void();

View File

@ -0,0 +1,89 @@
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateTeamSettingsRequestSchema,
ZUpdateTeamSettingsResponseSchema,
} from './update-team-settings.types';
export const updateTeamSettingsRoute = authenticatedProcedure
.input(ZUpdateTeamSettingsRequestSchema)
.output(ZUpdateTeamSettingsResponseSchema)
.mutation(async ({ ctx, input }) => {
const { user } = ctx;
const { teamId, data } = input;
const {
// Document related settings.
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
// Branding related settings.
// brandingEnabled,
// brandingLogo,
// brandingUrl,
// brandingCompanyDetails,
// brandingHidePoweredBy,
} = data;
if (Object.values(data).length === 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'No settings to update',
});
}
// Signatures will only be inherited if all are NULL.
if (
typedSignatureEnabled === false &&
uploadSignatureEnabled === false &&
drawSignatureEnabled === false
) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'At least one signature type must be enabled',
});
}
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery(teamId, user.id, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
});
if (!team) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update this team.',
});
}
await prisma.team.update({
where: {
id: teamId,
},
data: {
teamGlobalSettings: {
update: {
// Document related settings.
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
// Branding related settings.
// brandingEnabled,
// brandingLogo,
// brandingUrl,
// brandingCompanyDetails,
// brandingHidePoweredBy,
},
},
},
});
});

View File

@ -0,0 +1,32 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
/**
* Null = Inherit from organisation.
* Undefined = Do nothing
*/
export const ZUpdateTeamSettingsRequestSchema = z.object({
teamId: z.number(),
data: z.object({
// Document related settings.
documentVisibility: z.nativeEnum(DocumentVisibility).nullish(),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).nullish(),
includeSenderDetails: z.boolean().nullish(),
includeSigningCertificate: z.boolean().nullish(),
typedSignatureEnabled: z.boolean().nullish(),
uploadSignatureEnabled: z.boolean().nullish(),
drawSignatureEnabled: z.boolean().nullish(),
// Branding related settings.
// Todo: Current disabled for now.
// brandingEnabled: z.boolean().nullish(),
// brandingLogo: z.string().nullish(),
// brandingUrl: z.string().nullish(),
// brandingCompanyDetails: z.string().nullish(),
// brandingHidePoweredBy: z.boolean().nullish(),
}),
});
export const ZUpdateTeamSettingsResponseSchema = z.void();

View File

@ -0,0 +1,18 @@
import { updateTeam } from '@documenso/lib/server-only/team/update-team';
import { authenticatedProcedure } from '../trpc';
import { ZUpdateTeamRequestSchema, ZUpdateTeamResponseSchema } from './update-team.types';
export const updateTeamRoute = authenticatedProcedure
// .meta(updateTeamMeta)
.input(ZUpdateTeamRequestSchema)
.output(ZUpdateTeamResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, data } = input;
await updateTeam({
userId: ctx.user.id,
teamId,
data,
});
});

View File

@ -0,0 +1,23 @@
import { z } from 'zod';
import { ZTeamNameSchema, ZTeamUrlSchema } from './schema';
// export const updateTeamMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
// path: '/team/{teamId}',
// summary: 'Update team',
// description: 'Update an team',
// tags: ['team'],
// },
// };
export const ZUpdateTeamRequestSchema = z.object({
teamId: z.number(),
data: z.object({
name: ZTeamNameSchema,
url: ZTeamUrlSchema,
}),
});
export const ZUpdateTeamResponseSchema = z.void();

View File

@ -21,7 +21,6 @@ import { deleteTemplateDirectLink } from '@documenso/lib/server-only/template/de
import { duplicateTemplate } from '@documenso/lib/server-only/template/duplicate-template';
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { moveTemplateToTeam } from '@documenso/lib/server-only/template/move-template-to-team';
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
import { updateTemplate } from '@documenso/lib/server-only/template/update-template';
@ -43,8 +42,6 @@ import {
ZFindTemplatesResponseSchema,
ZGetTemplateByIdRequestSchema,
ZGetTemplateByIdResponseSchema,
ZMoveTemplateToTeamRequestSchema,
ZMoveTemplateToTeamResponseSchema,
ZToggleTemplateDirectLinkRequestSchema,
ZToggleTemplateDirectLinkResponseSchema,
ZUpdateTemplateRequestSchema,
@ -395,32 +392,6 @@ export const templateRouter = router({
return await toggleTemplateDirectLink({ userId, teamId, templateId, enabled });
}),
/**
* @public
*/
moveTemplateToTeam: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/move',
summary: 'Move template',
description: 'Move a template to a team',
tags: ['Template'],
},
})
.input(ZMoveTemplateToTeamRequestSchema)
.output(ZMoveTemplateToTeamResponseSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, teamId } = input;
const userId = ctx.user.id;
return await moveTemplateToTeam({
templateId,
teamId,
userId,
});
}),
/**
* @private
*/

View File

@ -193,16 +193,9 @@ export const ZGetTemplateByIdRequestSchema = z.object({
export const ZGetTemplateByIdResponseSchema = ZTemplateSchema;
export const ZMoveTemplateToTeamRequestSchema = z.object({
templateId: z.number().describe('The ID of the template to move to.'),
teamId: z.number().describe('The ID of the team to move the template to.'),
});
export const ZMoveTemplateToTeamResponseSchema = ZTemplateLiteSchema;
export const ZBulkSendTemplateMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
teamId: z.number(),
csv: z.string().min(1),
sendImmediately: z.boolean(),
});

View File

@ -83,7 +83,19 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
ctx: {
...ctx,
user: apiToken.user,
teamId: apiToken.teamId || undefined,
teamId: apiToken.teamId || 11111111111111, // TODO: @@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
// @@@@@@@@@@@@@@
session: null,
metadata: {
...ctx.metadata,

View File

@ -3,24 +3,19 @@ import { deleteWebhookById } from '@documenso/lib/server-only/webhooks/delete-we
import { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook';
import { getWebhookById } from '@documenso/lib/server-only/webhooks/get-webhook-by-id';
import { getWebhooksByTeamId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-team-id';
import { getWebhooksByUserId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-user-id';
import { authenticatedProcedure, router } from '../trpc';
import {
ZCreateWebhookMutationSchema,
ZDeleteWebhookMutationSchema,
ZEditWebhookMutationSchema,
ZGetTeamWebhooksQuerySchema,
ZGetWebhookByIdQuerySchema,
ZCreateWebhookRequestSchema,
ZDeleteWebhookRequestSchema,
ZEditWebhookRequestSchema,
ZGetTeamWebhooksRequestSchema,
ZGetWebhookByIdRequestSchema,
} from './schema';
export const webhookRouter = router({
getWebhooks: authenticatedProcedure.query(async ({ ctx }) => {
return await getWebhooksByUserId(ctx.user.id);
}),
getTeamWebhooks: authenticatedProcedure
.input(ZGetTeamWebhooksQuerySchema)
.input(ZGetTeamWebhooksRequestSchema)
.query(async ({ ctx, input }) => {
const { teamId } = input;
@ -28,7 +23,7 @@ export const webhookRouter = router({
}),
getWebhookById: authenticatedProcedure
.input(ZGetWebhookByIdQuerySchema)
.input(ZGetWebhookByIdRequestSchema)
.query(async ({ input, ctx }) => {
const { id, teamId } = input;
@ -40,7 +35,7 @@ export const webhookRouter = router({
}),
createWebhook: authenticatedProcedure
.input(ZCreateWebhookMutationSchema)
.input(ZCreateWebhookRequestSchema)
.mutation(async ({ input, ctx }) => {
const { enabled, eventTriggers, secret, webhookUrl, teamId } = input;
@ -55,7 +50,7 @@ export const webhookRouter = router({
}),
deleteWebhook: authenticatedProcedure
.input(ZDeleteWebhookMutationSchema)
.input(ZDeleteWebhookRequestSchema)
.mutation(async ({ input, ctx }) => {
const { id, teamId } = input;
@ -67,7 +62,7 @@ export const webhookRouter = router({
}),
editWebhook: authenticatedProcedure
.input(ZEditWebhookMutationSchema)
.input(ZEditWebhookRequestSchema)
.mutation(async ({ input, ctx }) => {
const { id, teamId, ...data } = input;

View File

@ -1,40 +1,40 @@
import { WebhookTriggerEvents } from '@prisma/client';
import { z } from 'zod';
export const ZGetTeamWebhooksQuerySchema = z.object({
export const ZGetTeamWebhooksRequestSchema = z.object({
teamId: z.number(),
});
export type TGetTeamWebhooksQuerySchema = z.infer<typeof ZGetTeamWebhooksQuerySchema>;
export type TGetTeamWebhooksRequestSchema = z.infer<typeof ZGetTeamWebhooksRequestSchema>;
export const ZCreateWebhookMutationSchema = z.object({
export const ZCreateWebhookRequestSchema = z.object({
webhookUrl: z.string().url(),
eventTriggers: z
.array(z.nativeEnum(WebhookTriggerEvents))
.min(1, { message: 'At least one event trigger is required' }),
secret: z.string().nullable(),
enabled: z.boolean(),
teamId: z.number().optional(),
teamId: z.number(),
});
export type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookMutationSchema>;
export type TCreateWebhookFormSchema = z.infer<typeof ZCreateWebhookRequestSchema>;
export const ZGetWebhookByIdQuerySchema = z.object({
export const ZGetWebhookByIdRequestSchema = z.object({
id: z.string(),
teamId: z.number().optional(),
teamId: z.number(),
});
export type TGetWebhookByIdQuerySchema = z.infer<typeof ZGetWebhookByIdQuerySchema>;
export type TGetWebhookByIdRequestSchema = z.infer<typeof ZGetWebhookByIdRequestSchema>;
export const ZEditWebhookMutationSchema = ZCreateWebhookMutationSchema.extend({
export const ZEditWebhookRequestSchema = ZCreateWebhookRequestSchema.extend({
id: z.string(),
});
export type TEditWebhookMutationSchema = z.infer<typeof ZEditWebhookMutationSchema>;
export type TEditWebhookRequestSchema = z.infer<typeof ZEditWebhookRequestSchema>;
export const ZDeleteWebhookMutationSchema = z.object({
export const ZDeleteWebhookRequestSchema = z.object({
id: z.string(),
teamId: z.number().optional(),
teamId: z.number(),
});
export type TDeleteWebhookMutationSchema = z.infer<typeof ZDeleteWebhookMutationSchema>;
export type TDeleteWebhookRequestSchema = z.infer<typeof ZDeleteWebhookRequestSchema>;