Skip to content

Commit a6a71d3

Browse files
committed
Deleted projects now no longer throw a 500 ISE
1 parent 5ee342a commit a6a71d3

File tree

3 files changed

+133
-19
lines changed

3 files changed

+133
-19
lines changed

apps/backend/src/route-handlers/smart-request.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque
245245
};
246246
const queriesResults = await rawQueryAll(globalPrismaClient, bundledQueries);
247247
const project = await queriesResults.project;
248+
if (project === null) throw new KnownErrors.CurrentProjectNotFound(projectId); // this does allow one to probe whether a project exists or not, but that's fine
248249
const environmentConfig = await queriesResults.environmentRenderedConfig;
249250

250251
// As explained above, as a performance optimization we already fetch the user from the global database optimistically
@@ -267,10 +268,6 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque
267268
} else if (adminAccessToken) {
268269
// TODO put the assertion below into the bundled queries above (not so important because this path is quite rare)
269270
await extractUserFromAdminAccessToken({ token: adminAccessToken, projectId }); // assert that the admin token is valid
270-
if (!project) {
271-
// this happens if the project is still in the user's managedProjectIds, but has since been deleted
272-
throw new KnownErrors.InvalidProjectForAdminAccessToken();
273-
}
274271
} else {
275272
switch (requestType) {
276273
case "client": {
@@ -293,12 +290,6 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque
293290
}
294291
}
295292
}
296-
297-
if (!project) {
298-
// This happens when the JWT tokens are still valid, but the project has been deleted
299-
// note that we do the check only here as we don't want to leak whether a project exists or not unless its keys have been shown to be valid
300-
throw new KnownErrors.ProjectNotFound(projectId);
301-
}
302293
if (!tenancy) {
303294
throw new KnownErrors.BranchDoesNotExist(branchId);
304295
}

apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts

Lines changed: 118 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { isBase64Url } from "@stackframe/stack-shared/dist/utils/bytes";
22
import { it } from "../../../../helpers";
3-
import { Auth, InternalProjectKeys, Project, backendContext, niceBackendFetch } from "../../../backend-helpers";
3+
import { Auth, InternalApiKey, InternalProjectKeys, Project, backendContext, niceBackendFetch } from "../../../backend-helpers";
44

55

66
// TODO some of the tests here test /api/v1/projects/current, the others test /api/v1/internal/projects/current. We should split them into different test files
@@ -1231,6 +1231,109 @@ it("deletes a project with users, teams, and permissions", async ({ expect }) =>
12311231
"headers": Headers { <some fields may have been hidden> },
12321232
}
12331233
`);
1234+
1235+
// make sure that the project no longer exists
1236+
const getProjectResponse = await niceBackendFetch(`/api/v1/projects/current`, {
1237+
accessType: "admin",
1238+
method: "GET",
1239+
});
1240+
expect(getProjectResponse).toMatchInlineSnapshot(`
1241+
NiceResponse {
1242+
"status": 400,
1243+
"body": {
1244+
"code": "CURRENT_PROJECT_NOT_FOUND",
1245+
"details": { "project_id": "<stripped UUID>" },
1246+
"error": "The current project with ID <stripped UUID> was not found. Please check the value of the x-stack-project-id header.",
1247+
},
1248+
"headers": Headers {
1249+
"x-stack-known-error": "CURRENT_PROJECT_NOT_FOUND",
1250+
<some fields may have been hidden>,
1251+
},
1252+
}
1253+
`);
1254+
});
1255+
1256+
it("does not allow accessing a current project that doesn't exist", async ({ expect }) => {
1257+
backendContext.set({
1258+
projectKeys: {
1259+
projectId: "does-not-exist",
1260+
},
1261+
});
1262+
const response = await niceBackendFetch(`/api/v1/projects/current`, {
1263+
accessType: "admin",
1264+
method: "GET",
1265+
});
1266+
expect(response).toMatchInlineSnapshot(`
1267+
NiceResponse {
1268+
"status": 400,
1269+
"body": {
1270+
"code": "CURRENT_PROJECT_NOT_FOUND",
1271+
"details": { "project_id": "does-not-exist" },
1272+
"error": "The current project with ID does-not-exist was not found. Please check the value of the x-stack-project-id header.",
1273+
},
1274+
"headers": Headers {
1275+
"x-stack-known-error": "CURRENT_PROJECT_NOT_FOUND",
1276+
<some fields may have been hidden>,
1277+
},
1278+
}
1279+
`);
1280+
});
1281+
1282+
it("does not allow accessing a project with the wrong API keys", async ({ expect }) => {
1283+
await Project.createAndSwitch();
1284+
await InternalApiKey.createAndSetProjectKeys();
1285+
backendContext.set({
1286+
projectKeys: {
1287+
projectId: (backendContext.value.projectKeys as any).projectId,
1288+
publishableClientKey: "fake publishable client key",
1289+
secretServerKey: "fake secret server key",
1290+
superSecretAdminKey: "fake admin key",
1291+
}
1292+
});
1293+
const response = await niceBackendFetch(`/api/v1/projects/current`, {
1294+
accessType: "admin",
1295+
method: "GET",
1296+
});
1297+
expect(response).toMatchInlineSnapshot(`
1298+
NiceResponse {
1299+
"status": 401,
1300+
"body": {
1301+
"code": "INVALID_SUPER_SECRET_ADMIN_KEY",
1302+
"details": { "project_id": "<stripped UUID>" },
1303+
"error": "The super secret admin key is not valid for the project \\"<stripped UUID>\\". Does the project and/or the key exist?",
1304+
},
1305+
"headers": Headers {
1306+
"x-stack-known-error": "INVALID_SUPER_SECRET_ADMIN_KEY",
1307+
<some fields may have been hidden>,
1308+
},
1309+
}
1310+
`);
1311+
});
1312+
1313+
it("does not allow accessing a project without a project ID header", async ({ expect }) => {
1314+
backendContext.set({ projectKeys: "no-project" });
1315+
const response = await niceBackendFetch(`/api/v1/projects/current`, {
1316+
accessType: "admin",
1317+
method: "GET",
1318+
});
1319+
expect(response).toMatchInlineSnapshot(`
1320+
NiceResponse {
1321+
"status": 400,
1322+
"body": {
1323+
"code": "ACCESS_TYPE_WITHOUT_PROJECT_ID",
1324+
"details": { "request_type": "admin" },
1325+
"error": deindent\`
1326+
The x-stack-access-type header was 'admin', but the x-stack-project-id header was not provided.
1327+
1328+
For more information, see the docs on REST API authentication: https://docs.stack-auth.com/rest-api/overview#authentication
1329+
\`,
1330+
},
1331+
"headers": Headers {
1332+
"x-stack-known-error": "ACCESS_TYPE_WITHOUT_PROJECT_ID",
1333+
<some fields may have been hidden>,
1334+
},
1335+
}
1336+
`);
12341337
});
12351338

12361339
it("makes sure user have the correct managed project ID after project creation", async ({ expect }) => {
@@ -1247,17 +1350,23 @@ it("makes sure user have the correct managed project ID after project creation",
12471350
expect(projectIds[0]).toBe(projectId);
12481351
});
12491352

1250-
it("makes sure user don't have managed project ID after project deletion", async ({ expect }) => {
1353+
it("removes a deleted project from a user's managed project IDs", async ({ expect }) => {
12511354
backendContext.set({ projectKeys: InternalProjectKeys });
1252-
const { creatorUserId, adminAccessToken } = await Project.createAndGetAdminToken();
1355+
const { creatorUserId, adminAccessToken, projectId } = await Project.createAndGetAdminToken();
1356+
1357+
backendContext.set({ projectKeys: InternalProjectKeys });
1358+
const userResponse1 = await niceBackendFetch(`/api/v1/users/${creatorUserId}`, {
1359+
accessType: "server",
1360+
method: "GET",
1361+
});
1362+
const projectIds1 = userResponse1.body.server_metadata.managedProjectIds;
1363+
expect(projectIds1.length).toBe(1);
12531364

12541365
// Delete the project
1366+
backendContext.set({ projectKeys: { projectId, adminAccessToken } });
12551367
const deleteResponse = await niceBackendFetch(`/api/v1/internal/projects/current`, {
12561368
accessType: "admin",
12571369
method: "DELETE",
1258-
headers: {
1259-
'x-stack-admin-access-token': adminAccessToken,
1260-
}
12611370
});
12621371

12631372
expect(deleteResponse).toMatchInlineSnapshot(`
@@ -1270,12 +1379,12 @@ it("makes sure user don't have managed project ID after project deletion", async
12701379

12711380
backendContext.set({ projectKeys: InternalProjectKeys });
12721381

1273-
const userResponse = await niceBackendFetch(`/api/v1/users/${creatorUserId}`, {
1382+
const userResponse2 = await niceBackendFetch(`/api/v1/users/${creatorUserId}`, {
12741383
accessType: "server",
12751384
method: "GET",
12761385
});
1277-
const projectIds = userResponse.body.server_metadata.managedProjectIds;
1278-
expect(projectIds.length).toBe(0);
1386+
const projectIds2 = userResponse2.body.server_metadata.managedProjectIds;
1387+
expect(projectIds2.length).toBe(0);
12791388
});
12801389

12811390
it("makes sure other users are not affected by project deletion", async ({ expect }) => {

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,19 @@ const ProjectNotFound = createKnownErrorConstructor(
663663
(json: any) => [json.project_id] as const,
664664
);
665665

666+
const CurrentProjectNotFound = createKnownErrorConstructor(
667+
KnownError,
668+
"CURRENT_PROJECT_NOT_FOUND",
669+
(projectId: string) => [
670+
400,
671+
`The current project with ID ${projectId} was not found. Please check the value of the x-stack-project-id header.`,
672+
{
673+
project_id: projectId,
674+
},
675+
] as const,
676+
(json: any) => [json.project_id] as const,
677+
);
678+
666679
const BranchDoesNotExist = createKnownErrorConstructor(
667680
KnownError,
668681
"BRANCH_DOES_NOT_EXIST",
@@ -1456,6 +1469,7 @@ export const KnownErrors = {
14561469
ApiKeyNotFound,
14571470
PublicApiKeyCannotBeRevoked,
14581471
ProjectNotFound,
1472+
CurrentProjectNotFound,
14591473
BranchDoesNotExist,
14601474
SignUpNotEnabled,
14611475
PasswordAuthenticationNotEnabled,

0 commit comments

Comments
 (0)