mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 11:12:06 +10:00
fix: wip
This commit is contained in:
@ -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
|
||||
*
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
@ -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
|
||||
>;
|
||||
@ -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;
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
@ -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
|
||||
>;
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
>;
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
@ -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],
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
@ -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 },
|
||||
);
|
||||
};
|
||||
@ -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();
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
@ -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>;
|
||||
};
|
||||
@ -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>;
|
||||
@ -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>;
|
||||
};
|
||||
@ -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
|
||||
>;
|
||||
@ -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>;
|
||||
};
|
||||
@ -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
|
||||
>;
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -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
|
||||
>;
|
||||
@ -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),
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -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];
|
||||
77
packages/trpc/server/organisation-router/get-organisation.ts
Normal file
77
packages/trpc/server/organisation-router/get-organisation.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@ -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>;
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -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>;
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
@ -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();
|
||||
58
packages/trpc/server/organisation-router/router.ts
Normal file
58
packages/trpc/server/organisation-router/router.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
@ -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,
|
||||
})),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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>;
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
@ -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
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
@ -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,
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
|
||||
101
packages/trpc/server/team-router/create-team-groups.ts
Normal file
101
packages/trpc/server/team-router/create-team-groups.ts
Normal 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,
|
||||
})),
|
||||
});
|
||||
});
|
||||
28
packages/trpc/server/team-router/create-team-groups.types.ts
Normal file
28
packages/trpc/server/team-router/create-team-groups.types.ts
Normal 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();
|
||||
159
packages/trpc/server/team-router/create-team-members.ts
Normal file
159
packages/trpc/server/team-router/create-team-members.ts
Normal 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(),
|
||||
})),
|
||||
});
|
||||
};
|
||||
@ -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>;
|
||||
21
packages/trpc/server/team-router/create-team.ts
Normal file
21
packages/trpc/server/team-router/create-team.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
38
packages/trpc/server/team-router/create-team.types.ts
Normal file
38
packages/trpc/server/team-router/create-team.types.ts
Normal 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>;
|
||||
80
packages/trpc/server/team-router/delete-team-group.ts
Normal file
80
packages/trpc/server/team-router/delete-team-group.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
});
|
||||
18
packages/trpc/server/team-router/delete-team-group.types.ts
Normal file
18
packages/trpc/server/team-router/delete-team-group.types.ts
Normal 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();
|
||||
103
packages/trpc/server/team-router/delete-team-member.ts
Normal file
103
packages/trpc/server/team-router/delete-team-member.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
18
packages/trpc/server/team-router/delete-team-member.types.ts
Normal file
18
packages/trpc/server/team-router/delete-team-member.types.ts
Normal 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();
|
||||
18
packages/trpc/server/team-router/delete-team.ts
Normal file
18
packages/trpc/server/team-router/delete-team.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
17
packages/trpc/server/team-router/delete-team.types.ts
Normal file
17
packages/trpc/server/team-router/delete-team.types.ts
Normal 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();
|
||||
144
packages/trpc/server/team-router/find-team-groups.ts
Normal file
144
packages/trpc/server/team-router/find-team-groups.ts
Normal 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>;
|
||||
};
|
||||
47
packages/trpc/server/team-router/find-team-groups.types.ts
Normal file
47
packages/trpc/server/team-router/find-team-groups.types.ts
Normal 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>;
|
||||
23
packages/trpc/server/team-router/find-team-members.ts
Normal file
23
packages/trpc/server/team-router/find-team-members.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
27
packages/trpc/server/team-router/find-team-members.types.ts
Normal file
27
packages/trpc/server/team-router/find-team-members.types.ts
Normal 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>;
|
||||
15
packages/trpc/server/team-router/find-teams.ts
Normal file
15
packages/trpc/server/team-router/find-teams.ts
Normal 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 });
|
||||
});
|
||||
39
packages/trpc/server/team-router/find-teams.types.ts
Normal file
39
packages/trpc/server/team-router/find-teams.types.ts
Normal 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>;
|
||||
21
packages/trpc/server/team-router/get-team-members.ts
Normal file
21
packages/trpc/server/team-router/get-team-members.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
34
packages/trpc/server/team-router/get-team-members.types.ts
Normal file
34
packages/trpc/server/team-router/get-team-members.types.ts
Normal 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>;
|
||||
79
packages/trpc/server/team-router/get-team.ts
Normal file
79
packages/trpc/server/team-router/get-team.ts
Normal 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),
|
||||
};
|
||||
};
|
||||
39
packages/trpc/server/team-router/get-team.types.ts
Normal file
39
packages/trpc/server/team-router/get-team.types.ts
Normal 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>;
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
>;
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
76
packages/trpc/server/team-router/update-team-group.ts
Normal file
76
packages/trpc/server/team-router/update-team-group.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
});
|
||||
24
packages/trpc/server/team-router/update-team-group.types.ts
Normal file
24
packages/trpc/server/team-router/update-team-group.types.ts
Normal 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>;
|
||||
169
packages/trpc/server/team-router/update-team-member.ts
Normal file
169
packages/trpc/server/team-router/update-team-member.ts
Normal 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(),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
22
packages/trpc/server/team-router/update-team-member.types.ts
Normal file
22
packages/trpc/server/team-router/update-team-member.types.ts
Normal 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();
|
||||
89
packages/trpc/server/team-router/update-team-settings.ts
Normal file
89
packages/trpc/server/team-router/update-team-settings.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
18
packages/trpc/server/team-router/update-team.ts
Normal file
18
packages/trpc/server/team-router/update-team.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
23
packages/trpc/server/team-router/update-team.types.ts
Normal file
23
packages/trpc/server/team-router/update-team.types.ts
Normal 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();
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user