Skip to content

Commit

Permalink
Better errors for crud handlers (#143)
Browse files Browse the repository at this point in the history
* added more errors to team creation

* check teams in client get

* added db checks
  • Loading branch information
fomalhautb authored Jul 22, 2024
1 parent 7cca092 commit 0147213
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 54 deletions.
65 changes: 42 additions & 23 deletions apps/backend/src/app/api/v1/team-memberships/crud.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ensureTeamExist, ensureTeamMembershipDoesNotExist } from "@/lib/db-checks";
import { isTeamSystemPermission, teamSystemPermissionStringToDBType } from "@/lib/permissions";
import { prismaClient } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { getIdFromUserIdOrMe } from "@/route-handlers/utils";
import { KnownErrors } from "@stackframe/stack-shared";
import { teamMembershipsCrud } from "@stackframe/stack-shared/dist/interface/crud/team-memberships";
import { userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";

Expand All @@ -11,33 +14,49 @@ export const teamMembershipsCrudHandlers = createCrudHandlers(teamMembershipsCru
user_id: userIdOrMeSchema.required(),
}),
onCreate: async ({ auth, params }) => {
await prismaClient.teamMember.create({
data: {
projectUserId: params.user_id,
const userId = getIdFromUserIdOrMe(params.user_id, auth.user);

await prismaClient.$transaction(async (tx) => {
await ensureTeamExist(tx, {
projectId: auth.project.id,
teamId: params.team_id,
});

await ensureTeamMembershipDoesNotExist(tx, {
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,
},
teamId: params.team_id,
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,
},
}
}
}
};
}
}),
}
},
};
}
}),
}
},
});
});

return {};
},
onDelete: async ({ auth, params }) => {
Expand Down
11 changes: 3 additions & 8 deletions apps/backend/src/app/api/v1/team-permissions/crud.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { grantTeamPermission, listUserTeamPermissions, revokeTeamPermission } from "@/lib/permissions";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
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";
Expand Down Expand Up @@ -33,15 +34,9 @@ export const teamPermissionsCrudHandlers = createCrudHandlers(teamPermissionsCru
});
},
async onList({ auth, query }) {
let userId = query.user_id;
if (userId === 'me') {
if (!auth.user) {
throw new StatusError(StatusError.BadRequest, 'User authentication required to list permissions for user_id=me');
}
userId = auth.user.id;
}
const userId = getIdFromUserIdOrMe(query.user_id, auth.user);
if (auth.type === 'client' && userId !== auth.user?.id) {
throw new StatusError(StatusError.BadRequest, 'Client can only list permissions for their own user. user_id must be either "me" or the ID of the current user');
throw new StatusError(StatusError.Forbidden, 'Client can only list permissions for their own user. user_id must be either "me" or the ID of the current user');
}

return {
Expand Down
45 changes: 26 additions & 19 deletions apps/backend/src/app/api/v1/teams/crud.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ensureTeamMembershipExist } from "@/lib/db-checks";
import { isTeamSystemPermission, teamSystemPermissionStringToDBType } from "@/lib/permissions";
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 { teamsCrud } from "@stackframe/stack-shared/dist/interface/crud/teams";
import { userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";

function prismaToCrud(prisma: Prisma.TeamGetPayload<{}>) {
return {
Expand Down Expand Up @@ -72,18 +74,30 @@ export const teamsCrudHandlers = createCrudHandlers(teamsCrud, {
return prismaToCrud(db);
},
onRead: async ({ params, auth }) => {
const db = await prismaClient.team.findUnique({
where: {
projectId_teamId: {
const db = await prismaClient.$transaction(async (tx) => {
if (auth.type === 'client') {
await ensureTeamMembershipExist(tx, {
projectId: auth.project.id,
teamId: params.team_id,
userId: auth.user?.id ?? throwErr("Client must be logged in to read a team"),
});
}

const db = await prismaClient.team.findUnique({
where: {
projectId_teamId: {
projectId: auth.project.id,
teamId: params.team_id,
},
},
},
});
});

if (!db) {
throw new KnownErrors.TeamNotFound(params.team_id);
}
if (!db) {
throw new KnownErrors.TeamNotFound(params.team_id);
}

return db;
});

return prismaToCrud(db);
},
Expand Down Expand Up @@ -114,18 +128,11 @@ export const teamsCrudHandlers = createCrudHandlers(teamsCrud, {
});
},
onList: async ({ query, auth }) => {
if (auth.type === 'client') {
if (query.user_id !== 'me' && query.user_id !== auth.user?.id) {
throw new StatusError(StatusError.Forbidden, "You are only allowed to access your own teams with the client access token.");
}
const userId = getIdFromUserIdOrMe(query.user_id, auth.user);
if (auth.type === 'client' && userId !== auth.user?.id) {
throw new StatusError(StatusError.Forbidden, 'Client can only list teams for their own user. user_id must be either "me" or the ID of the current user');
}

if (query.user_id === 'me' && !auth.user) {
throw new KnownErrors.CannotGetOwnUserWithoutUser();
}

let userId = query.user_id === 'me' ? auth.user?.id : query.user_id;

const db = await prismaClient.team.findMany({
where: {
projectId: auth.project.id,
Expand Down
72 changes: 72 additions & 0 deletions apps/backend/src/lib/db-checks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { KnownErrors } from "@stackframe/stack-shared";
import { PrismaTransaction } from "./types";


async function _getTeamMembership(
tx: PrismaTransaction,
options: {
projectId: string,
teamId: string, userId: string,
}
) {
return await tx.teamMember.findUnique({
where: {
projectId_projectUserId_teamId: {
projectId: options.projectId,
projectUserId: options.userId,
teamId: options.teamId,
},
},
});
}

export async function ensureTeamMembershipExist(
tx: PrismaTransaction,
options: {
projectId: string,
teamId: string,
userId: string,
}
) {
const member = await _getTeamMembership(tx, options);

if (!member) {
throw new KnownErrors.TeamMembershipNotFound(options.teamId, options.userId);
}
}

export async function ensureTeamMembershipDoesNotExist(
tx: PrismaTransaction,
options: {
projectId: string,
teamId: string,
userId: string,
}
) {
const member = await _getTeamMembership(tx, options);

if (member) {
throw new KnownErrors.TeamMembershipAlreadyExists();
}
}

export async function ensureTeamExist(
tx: PrismaTransaction,
options: {
projectId: string,
teamId: string,
}
) {
const team = await tx.team.findUnique({
where: {
projectId_teamId: {
projectId: options.projectId,
teamId: options.teamId,
},
},
});

if (!team) {
throw new KnownErrors.TeamNotFound(options.teamId);
}
}
3 changes: 3 additions & 0 deletions apps/backend/src/lib/types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { PrismaClient } from "@prisma/client";

export type PrismaTransaction = Parameters<Parameters<PrismaClient['$transaction']>[0]>[0];
15 changes: 15 additions & 0 deletions apps/backend/src/route-handlers/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { KnownErrors } from "@stackframe/stack-shared";
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";

export function getIdFromUserIdOrMe(userId: string, user: UsersCrud['Server']['Read'] | undefined): string;
export function getIdFromUserIdOrMe(userId: string | undefined, user: UsersCrud['Server']['Read'] | undefined): string | undefined;
export function getIdFromUserIdOrMe(userId: string | undefined, user: UsersCrud['Server']['Read'] | undefined): string | undefined {
if (userId === "me") {
if (!user) {
throw new KnownErrors.CannotGetOwnUserWithoutUser();
}
return user.id;
}

return userId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ it("is not allowed to list permissions from the other users on the client", asyn
});
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"status": 403,
"body": "Client can only list permissions for their own user. user_id must be either \\"me\\" or the ID of the current user",
"headers": Headers { <some fields may have been hidden> },
}
Expand Down
Loading

0 comments on commit 0147213

Please sign in to comment.