diff --git a/src/packages/next/lib/api/schema/projects/collaborators/remove.ts b/src/packages/next/lib/api/schema/projects/collaborators/remove.ts index a1f0dcc238..f9aa7dedf5 100644 --- a/src/packages/next/lib/api/schema/projects/collaborators/remove.ts +++ b/src/packages/next/lib/api/schema/projects/collaborators/remove.ts @@ -3,14 +3,16 @@ import { z } from "../../../framework"; import { FailedAPIOperationSchema, OkAPIOperationSchema } from "../../common"; import { ProjectIdSchema } from "../common"; +import { AccountIdSchema } from "../../accounts/common"; // OpenAPI spec // export const RemoveProjectCollaboratorInputSchema = z .object({ project_id: ProjectIdSchema, + account_id: AccountIdSchema, }) - .describe("Remove a collaborator from a project."); + .describe("Remove a collaborator from an existing project."); export const RemoveProjectCollaboratorOutputSchema = z.union([ FailedAPIOperationSchema, diff --git a/src/packages/next/pages/api/v2/projects/collaborators/add.ts b/src/packages/next/pages/api/v2/projects/collaborators/add.ts index 0bd8ef805b..2bca1d39c7 100644 --- a/src/packages/next/pages/api/v2/projects/collaborators/add.ts +++ b/src/packages/next/pages/api/v2/projects/collaborators/add.ts @@ -40,10 +40,10 @@ async function handle(req, res) { } export default apiRoute({ - add: apiRouteOperation({ + addProjectCollaborator: apiRouteOperation({ method: "POST", openApiOperation: { - tags: ["Projects", "Administrators"], + tags: ["Projects", "Admin"], }, }) .input({ diff --git a/src/packages/next/pages/api/v2/projects/collaborators/remove.ts b/src/packages/next/pages/api/v2/projects/collaborators/remove.ts new file mode 100644 index 0000000000..7db70220a7 --- /dev/null +++ b/src/packages/next/pages/api/v2/projects/collaborators/remove.ts @@ -0,0 +1,61 @@ +/* +API endpoint to remove a user from an existing project. + +Permissions checks are performed by the underlying API call and are NOT +executed at this stage. + +*/ +import { db } from "@cocalc/database"; +import { remove_collaborators_from_projects } from "@cocalc/server/projects/collab"; + +import getAccountId from "lib/account/get-account"; +import getParams from "lib/api/get-params"; +import { apiRoute, apiRouteOperation } from "lib/api"; +import { OkStatus } from "lib/api/status"; +import { + RemoveProjectCollaboratorInputSchema, + RemoveProjectCollaboratorOutputSchema, +} from "lib/api/schema/projects/collaborators/remove"; + +async function handle(req, res) { + const { project_id, account_id } = getParams(req); + const client_account_id = await getAccountId(req); + + try { + if (!client_account_id) { + throw Error("must be signed in"); + } + + await remove_collaborators_from_projects( + db(), + client_account_id, + [account_id], + [project_id], + ); + + res.json(OkStatus); + } catch (err) { + res.json({ error: err.message }); + } +} + +export default apiRoute({ + removeProjectCollaborator: apiRouteOperation({ + method: "POST", + openApiOperation: { + tags: ["Projects", "Admin"], + }, + }) + .input({ + contentType: "application/json", + body: RemoveProjectCollaboratorInputSchema, + }) + .outputs([ + { + status: 200, + contentType: "application/json", + body: RemoveProjectCollaboratorOutputSchema, + }, + ]) + .handler(handle), +}); diff --git a/src/packages/next/pages/api/v2/shopping/cart/add.ts b/src/packages/next/pages/api/v2/shopping/cart/add.ts index 77341a2e89..8be37c4216 100644 --- a/src/packages/next/pages/api/v2/shopping/cart/add.ts +++ b/src/packages/next/pages/api/v2/shopping/cart/add.ts @@ -52,7 +52,7 @@ async function add(req): Promise { } export default apiRoute({ - add: apiRouteOperation({ + addCartItem: apiRouteOperation({ method: "POST", openApiOperation: { tags: ["Shopping"], diff --git a/src/packages/server/projects/collab.ts b/src/packages/server/projects/collab.ts index 1797bf5eff..c638f2f9ec 100644 --- a/src/packages/server/projects/collab.ts +++ b/src/packages/server/projects/collab.ts @@ -67,6 +67,44 @@ export async function add_collaborators_to_projects( } } +export async function remove_collaborators_from_projects( + db: PostgreSQL, + account_id: string, + accounts: string[], + projects: string[], // can be empty strings if tokens specified (since they determine project_id) +): Promise { + try { + // Ensure user is allowed to modify project(s) + // + await verify_write_access_to_projects(db, account_id, projects); + } catch (err) { + // Users can always remove themselves from a project. + // + if (accounts.length == 1 && account_id == accounts[0]) { + await verify_course_access_to_project(db, account_id, projects[0]); + } else { + throw err; + } + } + + /* Right now this function is called from outside typescript + (e.g., api from user), so we have to do extra type checking. + Also, the input is uuid's, which typescript can't check. */ + verify_types(account_id, accounts, projects); + + // Remove users from projects + // + for (const i in projects) { + const project_id: string = projects[i]; + const account_id: string = accounts[i]; + + await callback2(db.remove_user_from_project, { + project_id, + account_id, + }); + } +} + // This is only meant to be used here in support of // add_collaborators_to_projects -- do not export it. async function verify_write_access_to_projects(