Skip to content

Commit 247e03a

Browse files
Account Management API: Updated the /api/v2/projects/update API route to use userQuery instead of a constructed SQL query.
1 parent 439e003 commit 247e03a

File tree

4 files changed

+43
-70
lines changed

4 files changed

+43
-70
lines changed

src/packages/next/lib/api/schema/projects/update.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import { z } from "../../framework";
33
import { FailedAPIOperationSchema, OkAPIOperationSchema } from "../common";
44

55
import { ProjectIdSchema } from "./common";
6+
import { AccountIdSchema } from "../accounts/common";
67

78
// OpenAPI spec
89
//
910
export const UpdateProjectInputSchema = z
1011
.object({
12+
account_id: AccountIdSchema.describe(
13+
`**Administrators only**. If provided, this operation will be executed on behalf of
14+
the account corresponding to this id.`,
15+
).optional(),
1116
project_id: ProjectIdSchema,
1217
title: z
1318
.string()
@@ -35,7 +40,7 @@ export const UpdateProjectInputSchema = z
3540
.describe(
3641
`Update an existing project's title, name, and/or description. If the API client is an
3742
admin, they may act on any project. Otherwise, the client may only update projects
38-
for which they are listed as owners.`,
43+
for which they are listed as collaborators.`,
3944
);
4045

4146
export const UpdateProjectOutputSchema = z.union([

src/packages/next/pages/api/v2/accounts/set-name.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ async function get(req) {
3636

3737
// This user MUST be an admin:
3838
if (account_id && !(await userIsInGroup(client_account_id, "admin"))) {
39-
throw Error("Only admins are authorized to specify an account id.");
39+
throw Error(
40+
"The `account_id` field may only be specified by account administrators.",
41+
);
4042
}
4143

4244
return userQuery({

src/packages/next/pages/api/v2/projects/update.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,20 @@ async function get(req) {
3232
throw Error("Must be signed in to update project.");
3333
}
3434

35-
const { title, description, name, project_id } = getParams(req);
35+
const { account_id, project_id, title, description, name } = getParams(req);
3636

37-
// If the API client is an admin, they may act on any project. Otherwise, the client may
38-
// only update projects for which they are listed as owners.
37+
// If the API client is an admin, they may act on any project on behalf of any account.
38+
// Otherwise, the client may only update projects for which they are listed as
39+
// collaborators.
3940
//
40-
const acting_account_id = (await userIsInGroup(client_account_id, "admin"))
41-
? undefined
42-
: client_account_id;
41+
if (account_id && !(await userIsInGroup(client_account_id, "admin"))) {
42+
throw Error(
43+
"The `account_id` field may only be specified by account administrators.",
44+
);
45+
}
4346

4447
return setProject({
45-
acting_account_id,
48+
acting_account_id: account_id || client_account_id,
4649
project_id,
4750
project_update: {
4851
title,
Lines changed: 24 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,38 @@
11
/* Updates an existing project's name, title, and/or description. May be
22
restricted such that the query is executed as though by a specific account_id.
3-
*/
43
5-
import getPool from "@cocalc/database/pool";
6-
import { isValidUUID } from "@cocalc/util/misc";
4+
This function is simply a wrapper around the userQuery function.
5+
*/
6+
import userQuery from "@cocalc/database/user-query";
77

88
import { DBProject } from "./get";
99

1010
export default async function setProject({
11+
acting_account_id,
1112
project_id,
1213
project_update,
13-
acting_account_id,
1414
}: {
15+
// This function executes as though the account id below made the request; this has the
16+
// effect of enforcing an authorization check that the acting account is allowed to
17+
// modify the desired project.
18+
//
19+
acting_account_id: string;
1520
project_id: string;
1621
project_update: Omit<DBProject, "project_id">;
17-
18-
// If this parameter is NOT provided, the specified project will be updated
19-
// with NO authorization checks.
20-
//
21-
// If this parameter IS provided, this function will execute the project update query as
22-
// though the account id below had made the request; this has the effect of enforcing an
23-
// authorization check that the acting account is allowed to modify the desired project.
24-
//
25-
acting_account_id?: string;
2622
}): Promise<DBProject | undefined> {
27-
// Filter out any provided fields which are null or undefined (but allow empty strings)
28-
// and convert parameter map to an ordered array.
29-
//
30-
const updateFields = Object.entries(project_update).filter(
31-
([_, v]) => v ?? false,
32-
);
33-
34-
if (!updateFields.length) {
35-
return;
36-
}
37-
38-
// Create query param array and append project_id
39-
//
40-
const queryParams = updateFields.map(([k, v]) => v);
41-
queryParams.push(project_id);
42-
43-
const updateSubQuery = updateFields
44-
.map(([k, v], i) => `${k}=$${i + 1}`)
45-
.join(",");
46-
47-
let query = `UPDATE projects SET ${updateSubQuery} WHERE project_id=$${queryParams.length} AND deleted IS NOT TRUE`;
48-
49-
// If acting_account_id is provided, we restrict the projects which may be updated
50-
// to those for which the corresponding account is listed as an owner.
51-
//
52-
if (acting_account_id) {
53-
if (!isValidUUID(acting_account_id)) {
54-
throw Error("acting_account_id must be a UUIDv4");
55-
}
56-
57-
queryParams.push(acting_account_id);
58-
59-
// TODO: Update this to execute only on owned projects.
60-
//
61-
query += ` AND users ? $${queryParams.length} AND (users#>>'{${acting_account_id},hide}')::BOOLEAN IS NOT TRUE`;
62-
}
63-
64-
// Return updated fields
65-
//
66-
query += `RETURNING project_id, title, description, name`;
67-
68-
// Execute query
69-
//
70-
const pool = getPool();
71-
const queryResult = await pool.query(query, queryParams);
72-
console.log(queryResult);
73-
const { rows } = queryResult;
74-
return rows?.[0];
23+
const { description, title, name } = project_update;
24+
return userQuery({
25+
account_id: acting_account_id,
26+
query: {
27+
projects: {
28+
// Any provided values must be non-empty in order for userQuery to SET values
29+
// instead of fetching them.
30+
//
31+
project_id,
32+
...(name && { name }),
33+
...(title && { title }),
34+
...(description && { description }),
35+
},
36+
},
37+
});
7538
}

0 commit comments

Comments
 (0)