Skip to content

Commit

Permalink
Fix team creation on the server not automatically adding the current …
Browse files Browse the repository at this point in the history
…user (#266)

* add_current_user => creator_user_id

* added more tests

* added error checks

* removed getIdFromUserIdOrMe

---------

Co-authored-by: Konsti Wohlwend <[email protected]>
  • Loading branch information
fomalhautb and N2D4 committed Oct 1, 2024
1 parent ce46c16 commit d0b3d6e
Show file tree
Hide file tree
Showing 14 changed files with 188 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { usersCrudHandlers } from "@/app/api/v1/users/crud";
import { getProvider } from "@/oauth";
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 { connectedAccountAccessTokenCrud } from "@stackframe/stack-shared/dist/interface/crud/oauth";
import { yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors";
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
import { extractScopes } from "@stackframe/stack-shared/dist/utils/strings";
Expand All @@ -14,12 +13,10 @@ import { extractScopes } from "@stackframe/stack-shared/dist/utils/strings";
export const connectedAccountAccessTokenCrudHandlers = createLazyProxy(() =>createCrudHandlers(connectedAccountAccessTokenCrud, {
paramsSchema: yupObject({
provider_id: yupString().required(),
user_id: yupString().required(),
user_id: userIdOrMeSchema.required(),
}),
async onCreate({ auth, data, params }) {
const userId = getIdFromUserIdOrMe(params.user_id, auth.user);

if (auth.type === 'client' && auth.user?.id !== userId) {
if (auth.type === 'client' && auth.user?.id !== params.user_id) {
throw new StatusError(StatusError.Forbidden, "Client can only access its own connected accounts");
}

Expand All @@ -32,7 +29,7 @@ export const connectedAccountAccessTokenCrudHandlers = createLazyProxy(() =>crea
throw new KnownErrors.OAuthAccessTokenNotAvailableWithSharedOAuthKeys();
}

const user = await usersCrudHandlers.adminRead({ project: auth.project, user_id: userId });
const user = await usersCrudHandlers.adminRead({ project: auth.project, user_id: params.user_id });
if (!user.oauth_providers.map(x => x.id).includes(params.provider_id)) {
throw new KnownErrors.OAuthConnectionNotConnectedToUser();
}
Expand All @@ -44,7 +41,7 @@ export const connectedAccountAccessTokenCrudHandlers = createLazyProxy(() =>crea
projectId: auth.project.id,
oAuthProviderConfigId: params.provider_id,
projectUserOAuthAccount: {
projectUserId: userId,
projectUserId: params.user_id,
},
expiresAt: {
// is at least 5 minutes in the future
Expand All @@ -66,7 +63,7 @@ export const connectedAccountAccessTokenCrudHandlers = createLazyProxy(() =>crea
projectId: auth.project.id,
oAuthProviderConfigId: params.provider_id,
projectUserOAuthAccount: {
projectUserId: userId,
projectUserId: params.user_id,
}
},
});
Expand Down
28 changes: 11 additions & 17 deletions apps/backend/src/app/api/v1/team-member-profiles/crud.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ensureTeamExist, ensureTeamMembershipExists, ensureUserExist, ensureUserTeamPermissionExists } 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";
Expand Down Expand Up @@ -33,7 +32,6 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa
}),
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
Expand All @@ -47,7 +45,7 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa

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

if (userId !== currentUserId) {
if (query.user_id !== currentUserId) {
await ensureUserTeamPermissionExists(tx, {
project: auth.project,
teamId: query.team_id,
Expand All @@ -61,16 +59,16 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa
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 });
if (query.user_id) {
await ensureUserExist(tx, { projectId: auth.project.id, userId: query.user_id });
}
}

const db = await tx.teamMember.findMany({
where: {
projectId: auth.project.id,
teamId: query.team_id,
projectUserId: userId,
projectUserId: query.user_id,
},
orderBy: {
createdAt: 'asc',
Expand All @@ -88,11 +86,9 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa
},
onRead: async ({ auth, params }) => {
return await prismaClient.$transaction(async (tx) => {
const userId = getIdFromUserIdOrMe(params.user_id, auth.user);

if (auth.type === 'client') {
const currentUserId = auth.user?.id ?? throwErr(new KnownErrors.CannotGetOwnUserWithoutUser());
if (userId !== currentUserId) {
if (params.user_id !== currentUserId) {
await ensureUserTeamPermissionExists(tx, {
project: auth.project,
teamId: params.team_id,
Expand All @@ -104,13 +100,13 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa
}
}

await ensureTeamMembershipExists(tx, { projectId: auth.project.id, teamId: params.team_id, userId: userId });
await ensureTeamMembershipExists(tx, { projectId: auth.project.id, teamId: params.team_id, userId: params.user_id });

const db = await tx.teamMember.findUnique({
where: {
projectId_projectUserId_teamId: {
projectId: auth.project.id,
projectUserId: userId,
projectUserId: params.user_id,
teamId: params.team_id,
},
},
Expand All @@ -119,34 +115,32 @@ export const teamMemberProfilesCrudHandlers = createLazyProxy(() => createCrudHa

if (!db) {
// This should never happen because of the check above
throw new KnownErrors.TeamMembershipNotFound(params.team_id, userId);
throw new KnownErrors.TeamMembershipNotFound(params.team_id, params.user_id);
}

return prismaToCrud(db, await getUserLastActiveAtMillis(db.projectUser.projectUserId, db.projectUser.createdAt));
});
},
onUpdate: async ({ auth, data, params }) => {
return await prismaClient.$transaction(async (tx) => {
const userId = getIdFromUserIdOrMe(params.user_id, auth.user);

if (auth.type === 'client') {
const currentUserId = auth.user?.id ?? throwErr(new KnownErrors.CannotGetOwnUserWithoutUser());
if (userId !== currentUserId) {
if (params.user_id !== currentUserId) {
throw new StatusError(StatusError.Forbidden, 'Cannot update another user\'s profile');
}
}

await ensureTeamMembershipExists(tx, {
projectId: auth.project.id,
teamId: params.team_id,
userId,
userId: params.user_id,
});

const db = await tx.teamMember.update({
where: {
projectId_projectUserId_teamId: {
projectId: auth.project.id,
projectUserId: userId,
projectUserId: params.user_id,
teamId: params.team_id,
},
},
Expand Down
19 changes: 7 additions & 12 deletions apps/backend/src/app/api/v1/team-memberships/crud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { ensureTeamExist, ensureTeamMembershipExists, ensureTeamMembershipDoesNo
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 { 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";
Expand Down Expand Up @@ -57,8 +56,6 @@ export const teamMembershipsCrudHandlers = createLazyProxy(() => createCrudHandl
user_id: userIdOrMeSchema.required(),
}),
onCreate: async ({ auth, params }) => {
const userId = getIdFromUserIdOrMe(params.user_id, auth.user);

await prismaClient.$transaction(async (tx) => {
await ensureTeamExist(tx, {
projectId: auth.project.id,
Expand All @@ -68,14 +65,14 @@ export const teamMembershipsCrudHandlers = createLazyProxy(() => createCrudHandl
await ensureTeamMembershipDoesNotExist(tx, {
projectId: auth.project.id,
teamId: params.team_id,
userId,
userId: params.user_id
});

const user = await tx.projectUser.findUnique({
where: {
projectId_projectUserId: {
projectId: auth.project.id,
projectUserId: userId,
projectUserId: params.user_id,
},
},
});
Expand All @@ -87,14 +84,14 @@ export const teamMembershipsCrudHandlers = createLazyProxy(() => createCrudHandl
await addUserToTeam(tx, {
project: auth.project,
teamId: params.team_id,
userId,
userId: params.user_id,
type: 'member',
});
});

const data = {
team_id: params.team_id,
user_id: userId,
user_id: params.user_id,
};

await sendTeamMembershipCreatedWebhook({
Expand All @@ -105,15 +102,13 @@ export const teamMembershipsCrudHandlers = createLazyProxy(() => createCrudHandl
return data;
},
onDelete: async ({ auth, params }) => {
const userId = getIdFromUserIdOrMe(params.user_id, auth.user);

await prismaClient.$transaction(async (tx) => {
// Users are always allowed to remove themselves from a team
// Only users with the $remove_members permission can remove other users
if (auth.type === 'client') {
const currentUserId = auth.user?.id ?? throwErr(new KnownErrors.CannotGetOwnUserWithoutUser());

if (userId !== currentUserId) {
if (params.user_id !== currentUserId) {
await ensureUserTeamPermissionExists(tx, {
project: auth.project,
teamId: params.team_id,
Expand All @@ -128,7 +123,7 @@ export const teamMembershipsCrudHandlers = createLazyProxy(() => createCrudHandl
await ensureTeamMembershipExists(tx, {
projectId: auth.project.id,
teamId: params.team_id,
userId,
userId: params.user_id,
});

await tx.teamMember.delete({
Expand All @@ -146,7 +141,7 @@ export const teamMembershipsCrudHandlers = createLazyProxy(() => createCrudHandl
projectId: auth.project.id,
data: {
team_id: params.team_id,
user_id: userId,
user_id: params.user_id,
},
});
},
Expand Down
6 changes: 2 additions & 4 deletions apps/backend/src/app/api/v1/team-permissions/crud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { grantTeamPermission, listUserTeamPermissions, revokeTeamPermission } fr
import { ensureTeamMembershipExists, ensureUserTeamPermissionExists } from "@/lib/request-checks";
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 { teamPermissionsCrud } from '@stackframe/stack-shared/dist/interface/crud/team-permissions';
import { teamPermissionDefinitionIdSchema, userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
Expand Down Expand Up @@ -53,11 +52,10 @@ export const teamPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
});
},
async onList({ auth, query }) {
const userId = getIdFromUserIdOrMe(query.user_id, auth.user);
if (auth.type === 'client') {
const currentUserId = auth.user?.id || throwErr(new KnownErrors.CannotGetOwnUserWithoutUser());

if (userId !== currentUserId) {
if (query.user_id !== currentUserId) {
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');
}
}
Expand All @@ -68,7 +66,7 @@ export const teamPermissionsCrudHandlers = createLazyProxy(() => createCrudHandl
project: auth.project,
teamId: query.team_id,
permissionId: query.permission_id,
userId,
userId: query.user_id,
recursive: query.recursive === 'true',
}),
is_paginated: false,
Expand Down
33 changes: 24 additions & 9 deletions apps/backend/src/app/api/v1/teams/crud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import { ensureTeamExist, ensureTeamMembershipExists, ensureUserTeamPermissionEx
import { sendTeamCreatedWebhook, sendTeamDeletedWebhook, sendTeamUpdatedWebhook } from "@/lib/webhooks";
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 { validateBase64Image } from "@stackframe/stack-shared/dist/utils/base64";
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";
import { validateBase64Image } from "@stackframe/stack-shared/dist/utils/base64";


export function teamPrismaToCrud(prisma: Prisma.TeamGetPayload<{}>) {
Expand All @@ -28,12 +27,17 @@ export function teamPrismaToCrud(prisma: Prisma.TeamGetPayload<{}>) {
export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsCrud, {
querySchema: yupObject({
user_id: userIdOrMeSchema.optional().meta({ openapiField: { onlyShowInOperations: ['List'], description: 'Filter for the teams that the user is a member of. Can be either `me` or an ID. Must be `me` in the client API', exampleValue: 'me' } }),
add_current_user: yupString().oneOf(["true", "false"]).optional().meta({ openapiField: { onlyShowInOperations: ['Create'], description: "If to add the current user to the team. If this is not `true`, the newly created team will have no members. Notice that if you didn't specify `add_current_user=true` on the client side, the user cannot join the team again without re-adding them on the server side.", exampleValue: 'true' } }),
/* deprecated, use creator_user_id in the body instead */
add_current_user: yupString().oneOf(["true", "false"]).optional().meta({ openapiField: { onlyShowInOperations: ['Create'], hidden: true } }),
}),
paramsSchema: yupObject({
team_id: yupString().uuid().required(),
}),
onCreate: async ({ query, auth, data }) => {
if (data.creator_user_id && query.add_current_user) {
throw new StatusError(StatusError.BadRequest, "Cannot use both creator_user_id and add_current_user. add_current_user is deprecated, please only use creator_user_id in the body.");
}

if (auth.type === 'client' && !auth.user) {
throw new KnownErrors.UserAuthenticationRequired();
}
Expand All @@ -58,15 +62,27 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC
},
});

if (query.add_current_user === 'true') {
let addUserId: string | undefined;
if (data.creator_user_id) {
if (auth.type === 'client') {
const currentUserId = auth.user?.id ?? throwErr(new KnownErrors.CannotGetOwnUserWithoutUser());
if (data.creator_user_id !== currentUserId) {
throw new StatusError(StatusError.Forbidden, "You cannot add a user to the team as the creator that is not yourself on the client.");
}
}
addUserId = data.creator_user_id;
} else if (query.add_current_user === 'true') {
if (!auth.user) {
throw new StatusError(StatusError.Unauthorized, "You must be logged in to create a team with the current user as a member.");
}
addUserId = auth.user.id;
}

if (addUserId) {
await addUserToTeam(tx, {
project: auth.project,
teamId: db.teamId,
userId: auth.user.id,
userId: addUserId,
type: 'creator',
});
}
Expand Down Expand Up @@ -188,22 +204,21 @@ export const teamsCrudHandlers = createLazyProxy(() => createCrudHandlers(teamsC
});
},
onList: async ({ query, auth }) => {
const userId = getIdFromUserIdOrMe(query.user_id, auth.user);
if (auth.type === 'client') {
const currentUserId = auth.user?.id || throwErr(new KnownErrors.CannotGetOwnUserWithoutUser());

if (userId !== currentUserId) {
if (query.user_id !== currentUserId) {
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');
}
}

const db = await prismaClient.team.findMany({
where: {
projectId: auth.project.id,
...userId ? {
...query.user_id ? {
teamMembers: {
some: {
projectUserId: userId,
projectUserId: query.user_id,
},
},
} : {},
Expand Down
6 changes: 2 additions & 4 deletions apps/backend/src/app/api/v1/users/crud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -542,10 +542,8 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
`${data.display_name}'s Team` :
data.primary_email ?
`${data.primary_email}'s Team` :
"Personal Team"
},
query: {
add_current_user: "true",
"Personal Team",
creator_user_id: 'me',
},
project: auth.project,
user: result,
Expand Down
Loading

0 comments on commit d0b3d6e

Please sign in to comment.