Skip to content

Commit 4792aa5

Browse files
kfahad5607fahad-netmonfomalhautb
authored andcommitted
Fix: Improve error handling for Server API (stack-auth#170)
* Added entity checks to provide better errors in API for 'server' access type * Removed 'ensureUserTeamPermissionExist', changed permissionId type to string in 'ensureUserHasTeamPermission' * added different error types for user team permission --------- Co-authored-by: Fahad Khan <fahad.khan@net-mon.net> Co-authored-by: Zai Shi <zaishi00@outlook.com>
1 parent 5b9ee57 commit 4792aa5

File tree

9 files changed

+89
-29
lines changed

9 files changed

+89
-29
lines changed

apps/backend/src/app/api/v1/team-invitations/send-code/route.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ensureUserHasTeamPermission } from "@/lib/request-checks";
1+
import { ensureUserTeamPermissionExists } from "@/lib/request-checks";
22
import { prismaClient } from "@/prisma-client";
33
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
44
import { adaptSchema, clientOrHigherAuthTypeSchema, teamIdSchema, teamInvitationCallbackUrlSchema, teamInvitationEmailSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
@@ -30,11 +30,12 @@ export const POST = createSmartRouteHandler({
3030
async handler({ auth, body }) {
3131
await prismaClient.$transaction(async (tx) => {
3232
if (auth.type === "client") {
33-
await ensureUserHasTeamPermission(tx, {
33+
await ensureUserTeamPermissionExists(tx, {
3434
project: auth.project,
3535
userId: auth.user.id,
3636
teamId: body.team_id,
37-
permissionId: "$invite_members"
37+
permissionId: "$invite_members",
38+
errorType: 'required',
3839
});
3940
}
4041
});

apps/backend/src/app/api/v1/team-member-profiles/crud.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ensureTeamExist, ensureTeamMembershipExist, ensureUserExist, ensureUserHasTeamPermission } from "@/lib/request-checks";
1+
import { ensureTeamExist, ensureTeamMembershipExists, ensureUserExist, ensureUserTeamPermissionExists } from "@/lib/request-checks";
22
import { prismaClient } from "@/prisma-client";
33
import { createCrudHandlers } from "@/route-handlers/crud-handler";
44
import { getIdFromUserIdOrMe } from "@/route-handlers/utils";
@@ -43,14 +43,15 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa
4343
throw new StatusError(StatusError.BadRequest, 'team_id is required for access type client');
4444
}
4545

46-
await ensureTeamMembershipExist(tx, { projectId: auth.project.id, teamId: query.team_id, userId: currentUserId });
46+
await ensureTeamMembershipExists(tx, { projectId: auth.project.id, teamId: query.team_id, userId: currentUserId });
4747

4848
if (userId !== currentUserId) {
49-
await ensureUserHasTeamPermission(tx, {
49+
await ensureUserTeamPermissionExists(tx, {
5050
project: auth.project,
5151
teamId: query.team_id,
5252
userId: currentUserId,
5353
permissionId: '$read_members',
54+
errorType: 'required',
5455
});
5556
}
5657
} else {
@@ -85,15 +86,16 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa
8586
const userId = getIdFromUserIdOrMe(params.user_id, auth.user);
8687

8788
if (auth.type === 'client' && userId !== auth.user?.id) {
88-
await ensureUserHasTeamPermission(tx, {
89+
await ensureUserTeamPermissionExists(tx, {
8990
project: auth.project,
9091
teamId: params.team_id,
9192
userId: auth.user?.id ?? throwErr("Client must be authenticated"),
9293
permissionId: '$read_members',
94+
errorType: 'required',
9395
});
9496
}
9597

96-
await ensureTeamMembershipExist(tx, { projectId: auth.project.id, teamId: params.team_id, userId: userId });
98+
await ensureTeamMembershipExists(tx, { projectId: auth.project.id, teamId: params.team_id, userId: userId });
9799

98100
const db = await tx.teamMember.findUnique({
99101
where: {
@@ -122,7 +124,7 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa
122124
throw new StatusError(StatusError.Forbidden, 'Cannot update another user\'s profile');
123125
}
124126

125-
await ensureTeamMembershipExist(tx, {
127+
await ensureTeamMembershipExists(tx, {
126128
projectId: auth.project.id,
127129
teamId: params.team_id,
128130
userId: auth.user?.id ?? throwErr("Client must be authenticated"),

apps/backend/src/app/api/v1/team-memberships/crud.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ensureTeamExist, ensureTeamMembershipDoesNotExist, ensureUserHasTeamPermission } from "@/lib/request-checks";
1+
import { ensureTeamExist, ensureTeamMembershipExists, ensureTeamMembershipDoesNotExist, ensureUserTeamPermissionExists } from "@/lib/request-checks";
22
import { isTeamSystemPermission, teamSystemPermissionStringToDBType } from "@/lib/permissions";
33
import { prismaClient } from "@/prisma-client";
44
import { createCrudHandlers } from "@/route-handlers/crud-handler";
@@ -100,14 +100,21 @@ export const teamMembershipsCrudHandlers = createLazyProxy(() => createCrudHandl
100100
// Users are always allowed to remove themselves from a team
101101
// Only users with the $remove_members permission can remove other users
102102
if (auth.type === 'client' && userId !== auth.user?.id) {
103-
await ensureUserHasTeamPermission(tx, {
103+
await ensureUserTeamPermissionExists(tx, {
104104
project: auth.project,
105105
teamId: params.team_id,
106106
userId: auth.user?.id ?? throwErr('auth.user is null'),
107107
permissionId: "$remove_members",
108+
errorType: 'required',
108109
});
109110
}
110111

112+
await ensureTeamMembershipExists(tx, {
113+
projectId: auth.project.id,
114+
teamId: params.team_id,
115+
userId,
116+
});
117+
111118
await tx.teamMember.delete({
112119
where: {
113120
projectId_projectUserId_teamId: {

apps/backend/src/app/api/v1/team-permissions/crud.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { grantTeamPermission, listUserTeamPermissions, revokeTeamPermission } from "@/lib/permissions";
2+
import { ensureTeamMembershipExists, ensureUserTeamPermissionExists } from "@/lib/request-checks";
23
import { prismaClient } from "@/prisma-client";
34
import { createCrudHandlers } from "@/route-handlers/crud-handler";
45
import { getIdFromUserIdOrMe } from "@/route-handlers/utils";
@@ -21,6 +22,8 @@ export const teamPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
2122
}),
2223
async onCreate({ auth, params }) {
2324
return await prismaClient.$transaction(async (tx) => {
25+
await ensureTeamMembershipExists(tx, { projectId: auth.project.id, teamId: params.team_id, userId: params.user_id });
26+
2427
return await grantTeamPermission(tx, {
2528
project: auth.project,
2629
teamId: params.team_id,
@@ -31,6 +34,14 @@ export const teamPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
3134
},
3235
async onDelete({ auth, params }) {
3336
return await prismaClient.$transaction(async (tx) => {
37+
await ensureUserTeamPermissionExists(tx, {
38+
project: auth.project,
39+
teamId: params.team_id,
40+
userId: params.user_id,
41+
permissionId: params.permission_id,
42+
errorType: 'not-exist',
43+
});
44+
3445
return await revokeTeamPermission(tx, {
3546
project: auth.project,
3647
teamId: params.team_id,

apps/backend/src/app/api/v1/teams/crud.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ensureTeamExist, ensureTeamMembershipExist, ensureUserHasTeamPermission } from "@/lib/request-checks";
1+
import { ensureTeamExist, ensureTeamMembershipExists, ensureUserTeamPermissionExists } from "@/lib/request-checks";
22
import { sendTeamCreatedWebhook, sendTeamDeletedWebhook, sendTeamUpdatedWebhook } from "@/lib/webhooks";
33
import { prismaClient } from "@/prisma-client";
44
import { createCrudHandlers } from "@/route-handlers/crud-handler";
@@ -66,7 +66,7 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC
6666
onRead: async ({ params, auth }) => {
6767
const db = await prismaClient.$transaction(async (tx) => {
6868
if (auth.type === 'client') {
69-
await ensureTeamMembershipExist(tx, {
69+
await ensureTeamMembershipExists(tx, {
7070
projectId: auth.project.id,
7171
teamId: params.team_id,
7272
userId: auth.user?.id ?? throwErr('auth.user is null'),
@@ -94,11 +94,12 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC
9494
onUpdate: async ({ params, auth, data }) => {
9595
const db = await prismaClient.$transaction(async (tx) => {
9696
if (auth.type === 'client') {
97-
await ensureUserHasTeamPermission(tx, {
97+
await ensureUserTeamPermissionExists(tx, {
9898
project: auth.project,
9999
teamId: params.team_id,
100100
userId: auth.user?.id ?? throwErr('auth.user is null'),
101101
permissionId: "$update_team",
102+
errorType: 'required',
102103
});
103104
}
104105

@@ -130,13 +131,15 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC
130131
onDelete: async ({ params, auth }) => {
131132
await prismaClient.$transaction(async (tx) => {
132133
if (auth.type === 'client') {
133-
await ensureUserHasTeamPermission(tx, {
134+
await ensureUserTeamPermissionExists(tx, {
134135
project: auth.project,
135136
teamId: params.team_id,
136137
userId: auth.user?.id ?? throwErr('auth.user is null'),
137138
permissionId: "$delete_team",
139+
errorType: 'required',
138140
});
139141
}
142+
await ensureTeamExist(tx, { projectId: auth.project.id, teamId: params.team_id });
140143

141144
await tx.team.delete({
142145
where: {

apps/backend/src/app/api/v1/users/crud.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ensureTeamMembershipExists, ensureUserExist } from "@/lib/request-checks";
12
import { prismaClient } from "@/prisma-client";
23
import { createCrudHandlers } from "@/route-handlers/crud-handler";
34
import { BooleanTrue, Prisma } from "@prisma/client";
@@ -197,7 +198,17 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
197198
},
198199
onUpdate: async ({ auth, data, params }) => {
199200
const db = await prismaClient.$transaction(async (tx) => {
201+
await ensureUserExist(tx, { projectId: auth.project.id, userId: params.user_id });
202+
200203
if (data.selected_team_id !== undefined) {
204+
if (data.selected_team_id !== null) {
205+
await ensureTeamMembershipExists(tx, {
206+
projectId: auth.project.id,
207+
teamId: data.selected_team_id,
208+
userId: params.user_id,
209+
});
210+
}
211+
201212
await tx.teamMember.updateMany({
202213
where: {
203214
projectId: auth.project.id,
@@ -257,14 +268,18 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
257268
return result;
258269
},
259270
onDelete: async ({ auth, params }) => {
260-
await prismaClient.projectUser.delete({
261-
where: {
262-
projectId_projectUserId: {
263-
projectId: auth.project.id,
264-
projectUserId: params.user_id,
271+
await prismaClient.$transaction(async (tx) => {
272+
await ensureUserExist(tx, { projectId: auth.project.id, userId: params.user_id });
273+
274+
await tx.projectUser.delete({
275+
where: {
276+
projectId_projectUserId: {
277+
projectId: auth.project.id,
278+
projectUserId: params.user_id,
279+
},
265280
},
266-
},
267-
include: fullInclude,
281+
include: fullInclude,
282+
});
268283
});
269284

270285
await sendUserDeletedWebhook({

apps/backend/src/lib/request-checks.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ProxiedOAuthProviderType, StandardOAuthProviderType } from "@prisma/cli
22
import { KnownErrors } from "@stackframe/stack-shared";
33
import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects";
44
import { ProviderType, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/utils/oauth";
5-
import { TeamSystemPermission, listUserTeamPermissions } from "./permissions";
5+
import { listUserTeamPermissions } from "./permissions";
66
import { PrismaTransaction } from "./types";
77

88

@@ -24,7 +24,7 @@ async function _getTeamMembership(
2424
});
2525
}
2626

27-
export async function ensureTeamMembershipExist(
27+
export async function ensureTeamMembershipExists(
2828
tx: PrismaTransaction,
2929
options: {
3030
projectId: string,
@@ -77,16 +77,17 @@ export async function ensureTeamExist(
7777
}
7878
}
7979

80-
export async function ensureUserHasTeamPermission(
80+
export async function ensureUserTeamPermissionExists(
8181
tx: PrismaTransaction,
8282
options: {
8383
project: ProjectsCrud["Admin"]["Read"],
8484
teamId: string,
8585
userId: string,
86-
permissionId: TeamSystemPermission,
86+
permissionId: string,
87+
errorType: 'required' | 'not-exist',
8788
}
8889
) {
89-
await ensureTeamMembershipExist(tx, {
90+
await ensureTeamMembershipExists(tx, {
9091
projectId: options.project.id,
9192
teamId: options.teamId,
9293
userId: options.userId,
@@ -101,7 +102,11 @@ export async function ensureUserHasTeamPermission(
101102
});
102103

103104
if (result.length === 0) {
104-
throw new KnownErrors.TeamPermissionRequired(options.teamId, options.userId, options.permissionId);
105+
if (options.errorType === 'not-exist') {
106+
throw new KnownErrors.TeamPermissionNotFound(options.teamId, options.userId, options.permissionId);
107+
} else {
108+
throw new KnownErrors.TeamPermissionRequired(options.teamId, options.userId, options.permissionId);
109+
}
105110
}
106111
}
107112

packages/stack-shared/src/known-errors.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,21 @@ const TeamPermissionRequired = createKnownErrorConstructor(
10341034
(json) => [json.team_id, json.user_id, json.permission_id] as const,
10351035
);
10361036

1037+
const TeamPermissionNotFound = createKnownErrorConstructor(
1038+
KnownError,
1039+
"TEAM_PERMISSION_NOT_FOUND",
1040+
(teamId, userId, permissionId) => [
1041+
401,
1042+
`User ${userId} does not have permission ${permissionId} in team ${teamId}.`,
1043+
{
1044+
team_id: teamId,
1045+
user_id: userId,
1046+
permission_id: permissionId,
1047+
},
1048+
] as const,
1049+
(json) => [json.team_id, json.user_id, json.permission_id] as const,
1050+
);
1051+
10371052
const InvalidSharedOAuthProviderId = createKnownErrorConstructor(
10381053
KnownError,
10391054
"INVALID_SHARED_OAUTH_PROVIDER_ID",
@@ -1156,6 +1171,7 @@ export const KnownErrors = {
11561171
InvalidSharedOAuthProviderId,
11571172
InvalidStandardOAuthProviderId,
11581173
InvalidAuthorizationCode,
1174+
TeamPermissionNotFound,
11591175
} satisfies Record<string, KnownErrorConstructor<any, any>>;
11601176

11611177

packages/stack-shared/src/schema-fields.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export const userIdSchema = yupString().uuid().meta({ openapiField: { descriptio
185185
export const primaryEmailSchema = emailSchema.meta({ openapiField: { description: 'Primary email', exampleValue: 'johndoe@example.com' } });
186186
export const primaryEmailVerifiedSchema = yupBoolean().meta({ openapiField: { description: 'Whether the primary email has been verified to belong to this user', exampleValue: true } });
187187
export const userDisplayNameSchema = yupString().nullable().meta({ openapiField: { description: _displayNameDescription('user'), exampleValue: 'John Doe' } });
188-
export const selectedTeamIdSchema = yupString().meta({ openapiField: { description: 'ID of the team currently selected by the user', exampleValue: 'team-id' } });
188+
export const selectedTeamIdSchema = yupString().uuid().meta({ openapiField: { description: 'ID of the team currently selected by the user', exampleValue: 'team-id' } });
189189
export const profileImageUrlSchema = yupString().meta({ openapiField: { description: _profileImageUrlDescription('user'), exampleValue: 'https://example.com/image.jpg' } });
190190
export const signedUpAtMillisSchema = yupNumber().meta({ openapiField: { description: _signedUpAtMillisDescription, exampleValue: 1630000000000 } });
191191
export const userClientMetadataSchema = jsonSchema.meta({ openapiField: { description: _clientMetaDataDescription('user'), exampleValue: { key: 'value' } } });

0 commit comments

Comments
 (0)