Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix team creation on the server not automatically adding the current user #266

Merged
merged 6 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading