diff --git a/apps/backend/prisma/migrations/20240725161939_team_profiles/migration.sql b/apps/backend/prisma/migrations/20240725161939_team_profiles/migration.sql new file mode 100644 index 0000000000..3ef93928c2 --- /dev/null +++ b/apps/backend/prisma/migrations/20240725161939_team_profiles/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "TeamMember" ADD COLUMN "displayName" TEXT, +ADD COLUMN "profileImageUrl" TEXT; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 4111123b27..3950d5f08a 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -114,6 +114,9 @@ model TeamMember { projectUserId String @db.Uuid teamId String @db.Uuid + displayName String? + profileImageUrl String? + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -539,7 +542,6 @@ enum StandardOAuthProviderType { //#endregion - //#region Events model Event { diff --git a/apps/backend/src/app/api/v1/auth/sessions/current/refresh/route.tsx b/apps/backend/src/app/api/v1/auth/sessions/current/refresh/route.tsx index 62833d24b8..2393576381 100644 --- a/apps/backend/src/app/api/v1/auth/sessions/current/refresh/route.tsx +++ b/apps/backend/src/app/api/v1/auth/sessions/current/refresh/route.tsx @@ -37,7 +37,7 @@ export const POST = createSmartRouteHandler({ }, }, }); - console.log("AAAAAAAAA", sessionObj); + if (!sessionObj || (sessionObj.expiresAt && sessionObj.expiresAt < new Date())) { throw new KnownErrors.RefreshTokenNotFoundOrExpired(); } diff --git a/apps/backend/src/app/api/v1/email-templates/crud.tsx b/apps/backend/src/app/api/v1/email-templates/crud.tsx index 5df93f7b3e..5f61c06522 100644 --- a/apps/backend/src/app/api/v1/email-templates/crud.tsx +++ b/apps/backend/src/app/api/v1/email-templates/crud.tsx @@ -7,6 +7,7 @@ import { emailTemplateCrud, emailTemplateTypes } from "@stackframe/stack-shared/ import { yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { typedEntries } from "@stackframe/stack-shared/dist/utils/objects"; +import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; import { typedToLowercase, typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; @@ -19,7 +20,7 @@ function prismaToCrud(prisma: Prisma.EmailTemplateGetPayload<{}>, isDefault: boo }; } -export const emailTemplateCrudHandlers = createCrudHandlers(emailTemplateCrud, { +export const emailTemplateCrudHandlers = createLazyProxy(() => createCrudHandlers(emailTemplateCrud, { paramsSchema: yupObject({ type: yupString().oneOf(emailTemplateTypes).required(), }), @@ -123,4 +124,4 @@ export const emailTemplateCrudHandlers = createCrudHandlers(emailTemplateCrud, { is_paginated: false, }; } -}); \ No newline at end of file +})); \ No newline at end of file diff --git a/apps/backend/src/app/api/v1/internal/api-keys/crud.tsx b/apps/backend/src/app/api/v1/internal/api-keys/crud.tsx index d45a93b8c8..b9a28b23ff 100644 --- a/apps/backend/src/app/api/v1/internal/api-keys/crud.tsx +++ b/apps/backend/src/app/api/v1/internal/api-keys/crud.tsx @@ -4,8 +4,9 @@ import { KnownErrors } from "@stackframe/stack-shared"; import { apiKeysCrud } from "@stackframe/stack-shared/dist/interface/crud/api-keys"; import { yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; -export const apiKeyCrudHandlers = createPrismaCrudHandlers(apiKeysCrud, "apiKeySet", { +export const apiKeyCrudHandlers = createLazyProxy(() => createPrismaCrudHandlers(apiKeysCrud, "apiKeySet", { paramsSchema: yupObject({ api_key_id: yupString().uuid().required(), }), @@ -68,4 +69,4 @@ export const apiKeyCrudHandlers = createPrismaCrudHandlers(apiKeysCrud, "apiKeyS manually_revoked_at_millis: prisma.manuallyRevokedAt?.getTime(), }; }, -}); +})); diff --git a/apps/backend/src/app/api/v1/internal/projects/crud.tsx b/apps/backend/src/app/api/v1/internal/projects/crud.tsx index 21f9f6328f..0ed37286f3 100644 --- a/apps/backend/src/app/api/v1/internal/projects/crud.tsx +++ b/apps/backend/src/app/api/v1/internal/projects/crud.tsx @@ -5,10 +5,11 @@ import { KnownErrors } from "@stackframe/stack-shared"; import { internalProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { projectIdSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; -export const internalProjectsCrudHandlers = createCrudHandlers(internalProjectsCrud, { +export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHandlers(internalProjectsCrud, { paramsSchema: yupObject({ projectId: projectIdSchema.required(), }), @@ -177,4 +178,4 @@ export const internalProjectsCrudHandlers = createCrudHandlers(internalProjectsC is_paginated: false, } as const; } -}); +})); diff --git a/apps/backend/src/app/api/v1/projects/current/crud.tsx b/apps/backend/src/app/api/v1/projects/current/crud.tsx index a5f95f61f6..ce15d12c5d 100644 --- a/apps/backend/src/app/api/v1/projects/current/crud.tsx +++ b/apps/backend/src/app/api/v1/projects/current/crud.tsx @@ -5,9 +5,10 @@ import { createCrudHandlers } from "@/route-handlers/crud-handler"; import { projectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { yupObject } from "@stackframe/stack-shared/dist/schema-fields"; import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; -export const projectsCrudHandlers = createCrudHandlers(projectsCrud, { +export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(projectsCrud, { paramsSchema: yupObject({}), onUpdate: async ({ auth, data }) => { const oldProject = auth.project; @@ -271,4 +272,4 @@ export const projectsCrudHandlers = createCrudHandlers(projectsCrud, { onRead: async ({ auth }) => { return auth.project; }, -}); +})); diff --git a/apps/backend/src/app/api/v1/team-member-profiles/[team_id]/[user_id]/route.tsx b/apps/backend/src/app/api/v1/team-member-profiles/[team_id]/[user_id]/route.tsx new file mode 100644 index 0000000000..8370dd2b88 --- /dev/null +++ b/apps/backend/src/app/api/v1/team-member-profiles/[team_id]/[user_id]/route.tsx @@ -0,0 +1,4 @@ +import { teamMemberProfilesCrudHandlers } from "../../crud"; + +export const GET = teamMemberProfilesCrudHandlers.readHandler; +export const PATCH = teamMemberProfilesCrudHandlers.updateHandler; \ No newline at end of file diff --git a/apps/backend/src/app/api/v1/team-member-profiles/crud.tsx b/apps/backend/src/app/api/v1/team-member-profiles/crud.tsx new file mode 100644 index 0000000000..97d369d161 --- /dev/null +++ b/apps/backend/src/app/api/v1/team-member-profiles/crud.tsx @@ -0,0 +1,149 @@ +import { ensureTeamExist, ensureTeamMembershipExist, ensureUserExist, ensureUserHasTeamPermission } from "@/lib/request-checks"; +import { prismaClient } from "@/prisma-client"; +import { createCrudHandlers } from "@/route-handlers/crud-handler"; +import { getIdFromUserIdOrMe } from "@/route-handlers/utils"; +import { Prisma } from "@prisma/client"; +import { KnownErrors } from "@stackframe/stack-shared"; +import { teamMemberProfilesCrud } from "@stackframe/stack-shared/dist/interface/crud/team-member-profiles"; +import { userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; +import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; + +const fullInclude = { projectUser: true }; + +function prismaToCrud(prisma: Prisma.TeamMemberGetPayload<{ include: typeof fullInclude }>) { + return { + team_id: prisma.teamId, + user_id: prisma.projectUserId, + display_name: prisma.displayName ?? prisma.projectUser.displayName, + profile_image_url: prisma.profileImageUrl ?? prisma.projectUser.profileImageUrl, + }; +} + +export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHandlers(teamMemberProfilesCrud, { + querySchema: yupObject({ + user_id: userIdOrMeSchema.optional().meta({ openapiField: { onlyShowInOperations: ['List'] }}), + team_id: yupString().uuid().optional().meta({ openapiField: { onlyShowInOperations: ['List'] }}), + }), + paramsSchema: yupObject({ + team_id: yupString().uuid().required(), + user_id: userIdOrMeSchema.required(), + }), + onList: async ({ auth, query }) => { + return await prismaClient.$transaction(async (tx) => { + const userId = getIdFromUserIdOrMe(query.user_id, auth.user); + if (auth.type === 'client') { + // Client can only: + // - list users in their own team if they have the $read_members permission + // - list their own profile + + const currentUserId = auth.user?.id ?? throwErr("Client must be authenticated"); + + if (!query.team_id) { + throw new StatusError(StatusError.BadRequest, 'team_id is required for access type client'); + } + + await ensureTeamMembershipExist(tx, { projectId: auth.project.id, teamId: query.team_id, userId: currentUserId }); + + if (userId !== currentUserId) { + await ensureUserHasTeamPermission(tx, { + project: auth.project, + teamId: query.team_id, + userId: currentUserId, + permissionId: '$read_members', + }); + } + } else { + if (query.team_id) { + await ensureTeamExist(tx, { projectId: auth.project.id, teamId: query.team_id }); + } + if (userId) { + await ensureUserExist(tx, { projectId: auth.project.id, userId: userId }); + } + } + + const db = await tx.teamMember.findMany({ + where: { + projectId: auth.project.id, + teamId: query.team_id, + projectUserId: userId, + }, + orderBy: { + createdAt: 'asc', + }, + include: fullInclude, + }); + + return { + items: db.map(prismaToCrud), + is_paginated: false, + }; + }); + }, + onRead: async ({ auth, params }) => { + return await prismaClient.$transaction(async (tx) => { + const userId = getIdFromUserIdOrMe(params.user_id, auth.user); + + if (auth.type === 'client' && userId !== auth.user?.id) { + await ensureUserHasTeamPermission(tx, { + project: auth.project, + teamId: params.team_id, + userId: auth.user?.id ?? throwErr("Client must be authenticated"), + permissionId: '$read_members', + }); + } + + await ensureTeamMembershipExist(tx, { projectId: auth.project.id, teamId: params.team_id, userId: userId }); + + const db = await tx.teamMember.findUnique({ + where: { + projectId_projectUserId_teamId: { + projectId: auth.project.id, + projectUserId: userId, + teamId: params.team_id, + }, + }, + include: fullInclude, + }); + + if (!db) { + // This should never happen because of the check above + throw new KnownErrors.TeamMembershipNotFound(params.team_id, userId); + } + + return prismaToCrud(db); + }); + }, + onUpdate: async ({ auth, data, params }) => { + return await prismaClient.$transaction(async (tx) => { + const userId = getIdFromUserIdOrMe(params.user_id, auth.user); + + if (auth.type === 'client' && userId !== auth.user?.id) { + throw new StatusError(StatusError.Forbidden, 'Cannot update another user\'s profile'); + } + + await ensureTeamMembershipExist(tx, { + projectId: auth.project.id, + teamId: params.team_id, + userId: auth.user?.id ?? throwErr("Client must be authenticated"), + }); + + const db = await tx.teamMember.update({ + where: { + projectId_projectUserId_teamId: { + projectId: auth.project.id, + projectUserId: userId, + teamId: params.team_id, + }, + }, + data: { + displayName: data.display_name, + profileImageUrl: data.profile_image_url, + }, + include: fullInclude, + }); + + return prismaToCrud(db); + }); + }, +})); diff --git a/apps/backend/src/app/api/v1/team-member-profiles/route.tsx b/apps/backend/src/app/api/v1/team-member-profiles/route.tsx new file mode 100644 index 0000000000..5f8d2a52fe --- /dev/null +++ b/apps/backend/src/app/api/v1/team-member-profiles/route.tsx @@ -0,0 +1,3 @@ +import { teamMemberProfilesCrudHandlers } from "./crud"; + +export const GET = teamMemberProfilesCrudHandlers.listHandler; \ No newline at end of file diff --git a/apps/backend/src/app/api/v1/team-memberships/crud.tsx b/apps/backend/src/app/api/v1/team-memberships/crud.tsx index f7dc36c34b..f59ebe9576 100644 --- a/apps/backend/src/app/api/v1/team-memberships/crud.tsx +++ b/apps/backend/src/app/api/v1/team-memberships/crud.tsx @@ -6,9 +6,51 @@ import { getIdFromUserIdOrMe } from "@/route-handlers/utils"; import { teamMembershipsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-memberships"; import { userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { KnownErrors } from "@stackframe/stack-shared"; +import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; +import { PrismaTransaction } from "@/lib/types"; +import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; -export const teamMembershipsCrudHandlers = createCrudHandlers(teamMembershipsCrud, { +export async function addUserToTeam(tx: PrismaTransaction, options: { + project: ProjectsCrud['Admin']['Read'], + teamId: string, + userId: string, + type: 'member' | 'creator', +}) { + const permissionAttributeName = options.type === 'creator' ? 'team_creator_default_permissions' : 'team_member_default_permissions'; + + await tx.teamMember.create({ + data: { + projectUserId: options.userId, + teamId: options.teamId, + projectId: options.project.id, + directPermissions: { + create: options.project.config[permissionAttributeName].map((p) => { + if (isTeamSystemPermission(p.id)) { + return { + systemPermission: teamSystemPermissionStringToDBType(p.id), + }; + } else { + return { + permission: { + connect: { + projectConfigId_queryableId: { + projectConfigId: options.project.config.id, + queryableId: p.id, + }, + } + } + }; + } + }), + } + }, + }); +} + + +export const teamMembershipsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamMembershipsCrud, { paramsSchema: yupObject({ team_id: yupString().uuid().required(), user_id: userIdOrMeSchema.required(), @@ -28,33 +70,25 @@ export const teamMembershipsCrudHandlers = createCrudHandlers(teamMembershipsCru userId, }); - await tx.teamMember.create({ - data: { - projectUserId: userId, - teamId: params.team_id, - projectId: auth.project.id, - directPermissions: { - create: auth.project.config.team_member_default_permissions.map((p) => { - if (isTeamSystemPermission(p.id)) { - return { - systemPermission: teamSystemPermissionStringToDBType(p.id), - }; - } else { - return { - permission: { - connect: { - projectConfigId_queryableId: { - projectConfigId: auth.project.config.id, - queryableId: p.id, - }, - } - } - }; - } - }), - } + const user = await tx.projectUser.findUnique({ + where: { + projectId_projectUserId: { + projectId: auth.project.id, + projectUserId: userId, + }, }, }); + + if (!user) { + throw new KnownErrors.UserNotFound(); + } + + await addUserToTeam(tx, { + project: auth.project, + teamId: params.team_id, + userId, + type: 'member', + }); }); return {}; @@ -85,4 +119,4 @@ export const teamMembershipsCrudHandlers = createCrudHandlers(teamMembershipsCru }); }); }, -}); +})); diff --git a/apps/backend/src/app/api/v1/team-permission-definitions/crud.tsx b/apps/backend/src/app/api/v1/team-permission-definitions/crud.tsx index 2f70dc7fd7..094342e288 100644 --- a/apps/backend/src/app/api/v1/team-permission-definitions/crud.tsx +++ b/apps/backend/src/app/api/v1/team-permission-definitions/crud.tsx @@ -2,9 +2,10 @@ import { createTeamPermissionDefinition, deleteTeamPermissionDefinition, listTea import { prismaClient } from "@/prisma-client"; import { createCrudHandlers } from "@/route-handlers/crud-handler"; import { teamPermissionDefinitionsCrud } from '@stackframe/stack-shared/dist/interface/crud/team-permissions'; -import { teamPermissionDefinitionIdSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; +import { teamPermissionDefinitionIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields"; +import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; -export const teamPermissionDefinitionsCrudHandlers = createCrudHandlers(teamPermissionDefinitionsCrud, { +export const teamPermissionDefinitionsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamPermissionDefinitionsCrud, { paramsSchema: yupObject({ permission_id: teamPermissionDefinitionIdSchema.required(), }), @@ -41,4 +42,4 @@ export const teamPermissionDefinitionsCrudHandlers = createCrudHandlers(teamPerm }; }); }, -}); +})); diff --git a/apps/backend/src/app/api/v1/team-permissions/crud.tsx b/apps/backend/src/app/api/v1/team-permissions/crud.tsx index 1e33e1dc24..4d05f1d851 100644 --- a/apps/backend/src/app/api/v1/team-permissions/crud.tsx +++ b/apps/backend/src/app/api/v1/team-permissions/crud.tsx @@ -5,8 +5,9 @@ import { getIdFromUserIdOrMe } from "@/route-handlers/utils"; import { teamPermissionsCrud } from '@stackframe/stack-shared/dist/interface/crud/team-permissions'; import { teamPermissionDefinitionIdSchema, userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; -export const teamPermissionsCrudHandlers = createCrudHandlers(teamPermissionsCrud, { +export const teamPermissionsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamPermissionsCrud, { querySchema: yupObject({ team_id: yupString().uuid().optional().meta({ openapiField: { description: 'Filter with the team ID. If set, only the permissions of the members in a specific team will be returned.', exampleValue: 'cce084a3-28b7-418e-913e-c8ee6d802ea4' } }), user_id: userIdOrMeSchema.optional().meta({ openapiField: { description: 'Filter with the user ID. If set, only the permissions this user has will be returned. Client request must set `user_id=me`', exampleValue: 'me' } }), @@ -57,4 +58,4 @@ export const teamPermissionsCrudHandlers = createCrudHandlers(teamPermissionsCru }; }); }, -}); +})); diff --git a/apps/backend/src/app/api/v1/teams/crud.tsx b/apps/backend/src/app/api/v1/teams/crud.tsx index ffb710c501..842f3ed01c 100644 --- a/apps/backend/src/app/api/v1/teams/crud.tsx +++ b/apps/backend/src/app/api/v1/teams/crud.tsx @@ -10,6 +10,7 @@ import { teamsCrud } from "@stackframe/stack-shared/dist/interface/crud/teams"; import { userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; +import { addUserToTeam } from "../team-memberships/crud"; export function teamPrismaToCrud(prisma: Prisma.TeamGetPayload<{}>) { @@ -42,32 +43,12 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC if (!auth.user) { throw new StatusError(StatusError.Unauthorized, "You must be logged in to create a team with the current user as a member."); } - await tx.teamMember.create({ - data: { - projectId: auth.project.id, - projectUserId: auth.user.id, - teamId: db.teamId, - directPermissions: { - create: auth.project.config.team_creator_default_permissions.map((p) => { - if (isTeamSystemPermission(p.id)) { - return { - systemPermission: teamSystemPermissionStringToDBType(p.id), - }; - } else { - return { - permission: { - connect: { - projectConfigId_queryableId: { - projectConfigId: auth.project.config.id, - queryableId: p.id, - }, - } - } - }; - } - }), - }, - } + + await addUserToTeam(tx, { + project: auth.project, + teamId: db.teamId, + userId: auth.user.id, + type: 'creator', }); } diff --git a/apps/backend/src/lib/request-checks.tsx b/apps/backend/src/lib/request-checks.tsx index 44cc45ddba..6058c28aac 100644 --- a/apps/backend/src/lib/request-checks.tsx +++ b/apps/backend/src/lib/request-checks.tsx @@ -30,6 +30,8 @@ export async function ensureTeamMembershipExist( userId: string, } ) { + await ensureUserExist(tx, { projectId: options.projectId, userId: options.userId }); + const member = await _getTeamMembership(tx, options); if (!member) { @@ -99,4 +101,25 @@ export async function ensureUserHasTeamPermission( if (result.length === 0) { throw new KnownErrors.TeamPermissionRequired(options.teamId, options.userId, options.permissionId); } +} + +export async function ensureUserExist( + tx: PrismaTransaction, + options: { + projectId: string, + userId: string, + } +) { + const user = await tx.projectUser.findUnique({ + where: { + projectId_projectUserId: { + projectId: options.projectId, + projectUserId: options.userId, + }, + }, + }); + + if (!user) { + throw new KnownErrors.UserNotFound(); + } } \ No newline at end of file diff --git a/apps/dashboard/prisma/schema.prisma b/apps/dashboard/prisma/schema.prisma index 4111123b27..3950d5f08a 100644 --- a/apps/dashboard/prisma/schema.prisma +++ b/apps/dashboard/prisma/schema.prisma @@ -114,6 +114,9 @@ model TeamMember { projectUserId String @db.Uuid teamId String @db.Uuid + displayName String? + profileImageUrl String? + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -539,7 +542,6 @@ enum StandardOAuthProviderType { //#endregion - //#region Events model Event { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts new file mode 100644 index 0000000000..dcc4643644 --- /dev/null +++ b/apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts @@ -0,0 +1,249 @@ +import { createMailbox, it } from "../../../../helpers"; +import { Auth, Team, backendContext, niceBackendFetch } from "../../../backend-helpers"; + +async function createTeam() { + const { userId: userId1 } = await Auth.Otp.signIn(); + backendContext.set({ mailbox: createMailbox() }); + + const { userId: userId2 } = await Auth.Otp.signIn(); + backendContext.set({ mailbox: createMailbox() }); + + const { userId: userId3 } = await Auth.Otp.signIn(); + + // update names of users + await niceBackendFetch(`/api/v1/users/${userId1}`, { + accessType: "server", + method: "PATCH", + body: { + display_name: "User 1", + }, + }); + + await niceBackendFetch(`/api/v1/users/${userId2}`, { + accessType: "server", + method: "PATCH", + body: { + display_name: "User 2", + }, + }); + + await niceBackendFetch(`/api/v1/users/${userId3}`, { + accessType: "server", + method: "PATCH", + body: { + display_name: "User 3", + }, + }); + + const { teamId } = await Team.create(); + + // Add members to team + await niceBackendFetch(`/api/v1/team-memberships/${teamId}/${userId1}`, { + accessType: "server", + method: "POST", + body: {}, + }); + await niceBackendFetch(`/api/v1/team-memberships/${teamId}/${userId2}`, { + accessType: "server", + method: "POST", + body: {}, + }); + + return { teamId, userId1, userId2, currentUserId: userId3 }; +} + + +it("lists and updates member profiles in team", async ({ expect }) => { + const { teamId, userId1, userId2, currentUserId } = await createTeam(); + + // Must specify team_id + const response = await niceBackendFetch(`/api/v1/team-member-profiles`, { + accessType: "client", + method: "GET", + }); + expect(response).toMatchInlineSnapshot(` + NiceResponse { + "status": 400, + "body": "team_id is required for access type client", + "headers": Headers {