mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 10:42:01 +10:00
feat: add team user management endpoints to api (#1416)
## Description Adds user management capabilities to our current API. Allows for adding, removing, listing and updating members of a given team using a valid API token. ## Related Issue N/A ## Changes Made - Added an endpoint for inviting a team member - Added an endpoint for removing a team member - Added an endpoint for updating a team member - Added an endpoint for listing team members ## Testing Performed Tests were written for this feature request
This commit is contained in:
@ -12,10 +12,12 @@ import {
|
||||
ZDeleteFieldMutationSchema,
|
||||
ZDeleteRecipientMutationSchema,
|
||||
ZDownloadDocumentSuccessfulSchema,
|
||||
ZFindTeamMembersResponseSchema,
|
||||
ZGenerateDocumentFromTemplateMutationResponseSchema,
|
||||
ZGenerateDocumentFromTemplateMutationSchema,
|
||||
ZGetDocumentsQuerySchema,
|
||||
ZGetTemplatesQuerySchema,
|
||||
ZInviteTeamMemberMutationSchema,
|
||||
ZNoBodyMutationSchema,
|
||||
ZResendDocumentForSigningMutationSchema,
|
||||
ZSendDocumentForSigningMutationSchema,
|
||||
@ -26,13 +28,17 @@ import {
|
||||
ZSuccessfulGetDocumentResponseSchema,
|
||||
ZSuccessfulGetTemplateResponseSchema,
|
||||
ZSuccessfulGetTemplatesResponseSchema,
|
||||
ZSuccessfulInviteTeamMemberResponseSchema,
|
||||
ZSuccessfulRecipientResponseSchema,
|
||||
ZSuccessfulRemoveTeamMemberResponseSchema,
|
||||
ZSuccessfulResendDocumentResponseSchema,
|
||||
ZSuccessfulResponseSchema,
|
||||
ZSuccessfulSigningResponseSchema,
|
||||
ZSuccessfulUpdateTeamMemberResponseSchema,
|
||||
ZUnsuccessfulResponseSchema,
|
||||
ZUpdateFieldMutationSchema,
|
||||
ZUpdateRecipientMutationSchema,
|
||||
ZUpdateTeamMemberMutationSchema,
|
||||
} from './schema';
|
||||
|
||||
const c = initContract();
|
||||
@ -273,6 +279,61 @@ export const ApiContractV1 = c.router(
|
||||
},
|
||||
summary: 'Delete a field from a document',
|
||||
},
|
||||
|
||||
findTeamMembers: {
|
||||
method: 'GET',
|
||||
path: '/api/v1/team/:id/members',
|
||||
responses: {
|
||||
200: ZFindTeamMembersResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
500: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'List team members',
|
||||
},
|
||||
|
||||
inviteTeamMember: {
|
||||
method: 'POST',
|
||||
path: '/api/v1/team/:id/members/invite',
|
||||
body: ZInviteTeamMemberMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulInviteTeamMemberResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
500: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Invite a member to a team',
|
||||
},
|
||||
|
||||
updateTeamMember: {
|
||||
method: 'PUT',
|
||||
path: '/api/v1/team/:id/members/:memberId',
|
||||
body: ZUpdateTeamMemberMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulUpdateTeamMemberResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
500: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Update a team member',
|
||||
},
|
||||
|
||||
removeTeamMember: {
|
||||
method: 'DELETE',
|
||||
path: '/api/v1/team/:id/members/:memberId',
|
||||
body: null,
|
||||
responses: {
|
||||
200: ZSuccessfulRemoveTeamMemberResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
500: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Remove a member from a team',
|
||||
},
|
||||
},
|
||||
{
|
||||
baseHeaders: ZAuthorizationHeadersSchema,
|
||||
|
||||
@ -26,6 +26,8 @@ import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recip
|
||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
|
||||
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
|
||||
import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites';
|
||||
import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members';
|
||||
import type { CreateDocumentFromTemplateResponse } from '@documenso/lib/server-only/template/create-document-from-template';
|
||||
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
|
||||
import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy';
|
||||
@ -49,7 +51,12 @@ import {
|
||||
} from '@documenso/lib/universal/upload/server-actions';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import {
|
||||
DocumentDataType,
|
||||
DocumentStatus,
|
||||
SigningStatus,
|
||||
TeamMemberRole,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
import { ApiContractV1 } from './contract';
|
||||
import { authenticatedMiddleware } from './middleware/authenticated';
|
||||
@ -1279,4 +1286,270 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
findTeamMembers: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: teamId } = args.params;
|
||||
|
||||
if (team?.id !== Number(teamId)) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
message: 'You are not authorized to perform actions against this team.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const self = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (self?.role !== TeamMemberRole.ADMIN) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
message: 'You are not authorized to perform actions against this team.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const members = await prisma.teamMember.findMany({
|
||||
where: {
|
||||
teamId: team.id,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
members: members.map((member) => ({
|
||||
id: member.id,
|
||||
email: member.user.email,
|
||||
role: member.role,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
inviteTeamMember: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: teamId } = args.params;
|
||||
|
||||
const { email, role } = args.body;
|
||||
|
||||
if (team?.id !== Number(teamId)) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
message: 'You are not authorized to perform actions against this team.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const self = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (self?.role !== TeamMemberRole.ADMIN) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
message: 'You are not authorized to perform actions against this team.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const hasAlreadyBeenInvited = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
teamId: team.id,
|
||||
user: {
|
||||
email,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (hasAlreadyBeenInvited) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'This user has already been invited to the team',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await createTeamMemberInvites({
|
||||
userId: user.id,
|
||||
userName: user.name ?? '',
|
||||
teamId: team.id,
|
||||
invitations: [
|
||||
{
|
||||
email,
|
||||
role,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'An invite has been sent to the member',
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
updateTeamMember: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: teamId, memberId } = args.params;
|
||||
|
||||
const { role } = args.body;
|
||||
|
||||
if (team?.id !== Number(teamId)) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
message: 'You are not authorized to perform actions against this team.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const self = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (self?.role !== TeamMemberRole.ADMIN) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
message: 'You are not authorized to perform actions against this team.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
id: Number(memberId),
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'The provided member id does not exist.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const updatedMember = await prisma.teamMember.update({
|
||||
where: {
|
||||
id: member.id,
|
||||
},
|
||||
data: {
|
||||
role,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
id: updatedMember.id,
|
||||
email: updatedMember.user.email,
|
||||
role: updatedMember.role,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
removeTeamMember: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: teamId, memberId } = args.params;
|
||||
|
||||
if (team?.id !== Number(teamId)) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
message: 'You are not authorized to perform actions against this team.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const self = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (self?.role !== TeamMemberRole.ADMIN) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
message: 'You are not authorized to perform actions against this team.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
id: Number(memberId),
|
||||
teamId: Number(teamId),
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Member not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (team.ownerUserId === member.userId) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
message: 'You cannot remove the owner of the team',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (member.userId === user.id) {
|
||||
return {
|
||||
status: 403,
|
||||
body: {
|
||||
message: 'You cannot remove yourself from the team',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await deleteTeamMembers({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
teamMemberIds: [member.id],
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
id: member.id,
|
||||
email: member.user.email,
|
||||
role: member.role,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
TeamMemberRole,
|
||||
TemplateType,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
@ -532,3 +533,41 @@ export const ZGetTemplatesQuerySchema = z.object({
|
||||
page: z.coerce.number().min(1).optional().default(1),
|
||||
perPage: z.coerce.number().min(1).optional().default(1),
|
||||
});
|
||||
|
||||
export const ZFindTeamMembersResponseSchema = z.object({
|
||||
members: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
email: z.string().email(),
|
||||
role: z.nativeEnum(TeamMemberRole),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const ZInviteTeamMemberMutationSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.transform((email) => email.toLowerCase()),
|
||||
role: z.nativeEnum(TeamMemberRole).optional().default(TeamMemberRole.MEMBER),
|
||||
});
|
||||
|
||||
export const ZSuccessfulInviteTeamMemberResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
export const ZUpdateTeamMemberMutationSchema = z.object({
|
||||
role: z.nativeEnum(TeamMemberRole),
|
||||
});
|
||||
|
||||
export const ZSuccessfulUpdateTeamMemberResponseSchema = z.object({
|
||||
id: z.number(),
|
||||
email: z.string().email(),
|
||||
role: z.nativeEnum(TeamMemberRole),
|
||||
});
|
||||
|
||||
export const ZSuccessfulRemoveTeamMemberResponseSchema = z.object({
|
||||
id: z.number(),
|
||||
email: z.string().email(),
|
||||
role: z.nativeEnum(TeamMemberRole),
|
||||
});
|
||||
|
||||
278
packages/app-tests/e2e/api/v1/team-user-management.spec.ts
Normal file
278
packages/app-tests/e2e/api/v1/team-user-management.spec.ts
Normal file
@ -0,0 +1,278 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
ZFindTeamMembersResponseSchema,
|
||||
ZSuccessfulInviteTeamMemberResponseSchema,
|
||||
ZSuccessfulRemoveTeamMemberResponseSchema,
|
||||
ZSuccessfulUpdateTeamMemberResponseSchema,
|
||||
ZUnsuccessfulResponseSchema,
|
||||
} from '@documenso/api/v1/schema';
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
test.describe('Team API', () => {
|
||||
test('findTeamMembers: should list team members', async ({ request }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 3,
|
||||
});
|
||||
|
||||
const ownerMember = team.members.find((member) => member.userId === team.owner.id)!;
|
||||
|
||||
// Should not be undefined
|
||||
expect(ownerMember).toBeTruthy();
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: team.owner.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const response = await request.get(`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const parsed = ZFindTeamMembersResponseSchema.safeParse(data);
|
||||
|
||||
const safeData = parsed.success ? parsed.data : null;
|
||||
|
||||
expect(parsed.success).toBeTruthy();
|
||||
|
||||
expect(safeData!.members).toHaveLength(4); // Owner + 3 members
|
||||
expect(safeData!.members[0]).toHaveProperty('id');
|
||||
expect(safeData!.members[0]).toHaveProperty('email');
|
||||
expect(safeData!.members[0]).toHaveProperty('role');
|
||||
|
||||
expect(safeData!.members).toContainEqual({
|
||||
id: ownerMember.id,
|
||||
email: ownerMember.user.email,
|
||||
role: ownerMember.role,
|
||||
});
|
||||
});
|
||||
|
||||
test('inviteTeamMember: should invite a new team member', async ({ request }) => {
|
||||
const team = await seedTeam();
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: team.owner.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const newUser = await seedUser();
|
||||
|
||||
const response = await request.post(
|
||||
`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members/invite`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
email: newUser.email,
|
||||
role: TeamMemberRole.MEMBER,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const parsed = ZSuccessfulInviteTeamMemberResponseSchema.safeParse(data);
|
||||
|
||||
const safeData = parsed.success ? parsed.data : null;
|
||||
|
||||
expect(parsed.success).toBeTruthy();
|
||||
expect(safeData!.message).toBe('An invite has been sent to the member');
|
||||
|
||||
const invite = await prisma.teamMemberInvite.findFirst({
|
||||
where: {
|
||||
email: newUser.email,
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(invite).toBeTruthy();
|
||||
});
|
||||
|
||||
test('updateTeamMember: should update a team member role', async ({ request }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 3,
|
||||
});
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: team.owner.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const member = team.members.find((member) => member.role === TeamMemberRole.MEMBER)!;
|
||||
|
||||
// Should not be undefined
|
||||
expect(member).toBeTruthy();
|
||||
|
||||
const response = await request.put(
|
||||
`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members/${member.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const parsed = ZSuccessfulUpdateTeamMemberResponseSchema.safeParse(data);
|
||||
|
||||
const safeData = parsed.success ? parsed.data : null;
|
||||
|
||||
expect(parsed.success).toBeTruthy();
|
||||
|
||||
expect(safeData!.id).toBe(member.id);
|
||||
expect(safeData!.email).toBe(member.user.email);
|
||||
expect(safeData!.role).toBe(TeamMemberRole.ADMIN);
|
||||
});
|
||||
|
||||
test('removeTeamMember: should remove a team member', async ({ request }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 3,
|
||||
});
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: team.owner.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const member = team.members.find((member) => member.role === TeamMemberRole.MEMBER)!;
|
||||
|
||||
// Should not be undefined
|
||||
expect(member).toBeTruthy();
|
||||
|
||||
const response = await request.delete(
|
||||
`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members/${member.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const parsed = ZSuccessfulRemoveTeamMemberResponseSchema.safeParse(data);
|
||||
|
||||
const safeData = parsed.success ? parsed.data : null;
|
||||
|
||||
expect(parsed.success).toBeTruthy();
|
||||
|
||||
expect(safeData!.id).toBe(member.id);
|
||||
expect(safeData!.email).toBe(member.user.email);
|
||||
expect(safeData!.role).toBe(member.role);
|
||||
|
||||
const removedMemberCount = await prisma.teamMember.count({
|
||||
where: {
|
||||
id: member.id,
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(removedMemberCount).toBe(0);
|
||||
});
|
||||
|
||||
test('removeTeamMember: should not remove team owner', async ({ request }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 3,
|
||||
});
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: team.owner.id,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const ownerMember = team.members.find((member) => member.userId === team.owner.id)!;
|
||||
|
||||
// Should not be undefined
|
||||
expect(ownerMember).toBeTruthy();
|
||||
|
||||
const response = await request.delete(
|
||||
`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members/${ownerMember.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.status()).toBe(403);
|
||||
|
||||
const parsed = ZUnsuccessfulResponseSchema.safeParse(await response.json());
|
||||
|
||||
expect(parsed.success).toBeTruthy();
|
||||
});
|
||||
|
||||
test('removeTeamMember: should not remove self', async ({ request }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 3,
|
||||
});
|
||||
|
||||
const member = team.members.find((member) => member.role === TeamMemberRole.MEMBER)!;
|
||||
|
||||
// Make our non-owner member an admin
|
||||
await prisma.teamMember.update({
|
||||
where: {
|
||||
id: member.id,
|
||||
},
|
||||
data: {
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
});
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: member.userId,
|
||||
teamId: team.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const response = await request.delete(
|
||||
`${WEBAPP_BASE_URL}/api/v1/team/${team.id}/members/${member.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.status()).toBe(403);
|
||||
|
||||
const parsed = ZUnsuccessfulResponseSchema.safeParse(await response.json());
|
||||
|
||||
expect(parsed.success).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -5,9 +5,9 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test:dev": "playwright test",
|
||||
"test-ui:dev": "playwright test --ui",
|
||||
"test:e2e": "start-server-and-test \"npm run start -w @documenso/web\" http://localhost:3000 \"playwright test\""
|
||||
"test:dev": "NODE_OPTIONS=--experimental-require-module playwright test",
|
||||
"test-ui:dev": "NODE_OPTIONS=--experimental-require-module playwright test --ui",
|
||||
"test:e2e": "NODE_OPTIONS=--experimental-require-module start-server-and-test \"npm run start -w @documenso/web\" http://localhost:3000 \"playwright test\""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
||||
@ -51,7 +51,7 @@ export const createApiToken = async ({
|
||||
name: tokenName,
|
||||
token: hashedToken,
|
||||
expires: expiresIn ? DateTime.now().plus(timeConstantsRecords[expiresIn]).toJSDate() : null,
|
||||
userId: teamId ? null : userId,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
@ -25,8 +25,7 @@ export const deleteTokenById = async ({ id, userId, teamId }: DeleteTokenByIdOpt
|
||||
return await prisma.apiToken.delete({
|
||||
where: {
|
||||
id,
|
||||
userId: teamId ? null : userId,
|
||||
teamId,
|
||||
teamId: teamId ?? null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ export const getUserTokens = async ({ userId }: GetUserTokensOptions) => {
|
||||
return await prisma.apiToken.findMany({
|
||||
where: {
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@ -23,7 +23,8 @@ export const getApiTokenByToken = async ({ token }: { token: string }) => {
|
||||
throw new Error('Expired token');
|
||||
}
|
||||
|
||||
if (apiToken.team) {
|
||||
// Handle a silly choice from many moons ago
|
||||
if (apiToken.team && !apiToken.user) {
|
||||
apiToken.user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: apiToken.team.ownerUserId,
|
||||
@ -33,9 +34,13 @@ export const getApiTokenByToken = async ({ token }: { token: string }) => {
|
||||
|
||||
const { user } = apiToken;
|
||||
|
||||
// This will never happen but we need to narrow types
|
||||
if (!user) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
return { ...apiToken, user };
|
||||
return {
|
||||
...apiToken,
|
||||
user,
|
||||
};
|
||||
};
|
||||
|
||||
@ -42,7 +42,7 @@ export const seedTeam = async ({
|
||||
createMany: {
|
||||
data: [teamOwner, ...teamMembers].map((user) => ({
|
||||
userId: user.id,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
role: user === teamOwner ? TeamMemberRole.ADMIN : TeamMemberRole.MEMBER,
|
||||
})),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user