Skip to content

Commit 648def7

Browse files
authored
Merge pull request #7724 from schrodingersket/feature/7665
Account Management API: Added new `/api/v2/projects/update` API route…
2 parents d382ea1 + c570481 commit 648def7

File tree

5 files changed

+183
-5
lines changed

5 files changed

+183
-5
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { z } from "../../framework";
2+
3+
import { FailedAPIOperationSchema, OkAPIOperationSchema } from "../common";
4+
5+
import { ProjectIdSchema } from "./common";
6+
import { AccountIdSchema } from "../accounts/common";
7+
8+
// OpenAPI spec
9+
//
10+
export const UpdateProjectInputSchema = z
11+
.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(),
16+
project_id: ProjectIdSchema,
17+
title: z
18+
.string()
19+
.describe(
20+
`The short title of the project. Should use no special formatting, except
21+
hashtags.`,
22+
)
23+
.optional(),
24+
description: z
25+
.string()
26+
.describe(
27+
`A longer textual description of the project. This can include hashtags and should
28+
be formatted using markdown.`,
29+
)
30+
.optional(),
31+
name: z
32+
.string()
33+
.describe(
34+
`The optional name of this project. Must be globally unique (up to case) across
35+
all projects with a given *owner*. It can be between 1 and 100 characters from
36+
a-z A-Z 0-9 period and dash.`,
37+
)
38+
.optional(),
39+
})
40+
.describe(
41+
`Update an existing project's title, name, and/or description. If the API client is an
42+
admin, they may act on any project. Otherwise, the client may only update projects
43+
for which they are listed as collaborators.`,
44+
);
45+
46+
export const UpdateProjectOutputSchema = z.union([
47+
FailedAPIOperationSchema,
48+
OkAPIOperationSchema,
49+
]);
50+
51+
export type UpdateProjectInput = z.infer<typeof UpdateProjectInputSchema>;
52+
export type UpdateProjectOutput = z.infer<typeof UpdateProjectOutputSchema>;

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ import getAccountId from "lib/account/get-account";
99
import getParams from "lib/api/get-params";
1010

1111
import { apiRoute, apiRouteOperation } from "lib/api";
12+
import { SuccessStatus } from "lib/api/status";
1213
import {
1314
SetAccountNameInputSchema,
1415
SetAccountNameOutputSchema,
1516
} from "lib/api/schema/accounts/set-name";
1617

1718
async function handle(req, res) {
1819
try {
19-
res.json(await get(req));
20+
await get(req);
21+
res.json(SuccessStatus);
2022
} catch (err) {
2123
res.json({ error: `${err.message ? err.message : err}` });
2224
return;
@@ -34,7 +36,9 @@ async function get(req) {
3436

3537
// This user MUST be an admin:
3638
if (account_id && !(await userIsInGroup(client_account_id, "admin"))) {
37-
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+
);
3842
}
3943

4044
return userQuery({
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
Set project title, description, etc.
3+
*/
4+
5+
import userIsInGroup from "@cocalc/server/accounts/is-in-group";
6+
import setProject from "@cocalc/server/projects/set-one";
7+
8+
import getAccountId from "lib/account/get-account";
9+
import getParams from "lib/api/get-params";
10+
11+
import { OkStatus } from "lib/api/status";
12+
import { apiRoute, apiRouteOperation } from "lib/api";
13+
import {
14+
UpdateProjectInputSchema,
15+
UpdateProjectOutputSchema,
16+
} from "lib/api/schema/projects/update";
17+
18+
async function handle(req, res) {
19+
try {
20+
await get(req);
21+
res.json(OkStatus);
22+
} catch (err) {
23+
res.json({ error: `${err.message ? err.message : err}` });
24+
return;
25+
}
26+
}
27+
28+
async function get(req) {
29+
const client_account_id = await getAccountId(req);
30+
31+
if (client_account_id == null) {
32+
throw Error("Must be signed in to update project.");
33+
}
34+
35+
const { account_id, project_id, title, description, name } = getParams(req);
36+
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.
40+
//
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+
}
46+
47+
return setProject({
48+
acting_account_id: account_id || client_account_id,
49+
project_id,
50+
project_update: {
51+
title,
52+
description,
53+
name,
54+
},
55+
});
56+
}
57+
58+
export default apiRoute({
59+
update: apiRouteOperation({
60+
method: "POST",
61+
openApiOperation: {
62+
tags: ["Projects", "Admin"],
63+
},
64+
})
65+
.input({
66+
contentType: "application/json",
67+
body: UpdateProjectInputSchema,
68+
})
69+
.outputs([
70+
{
71+
status: 200,
72+
contentType: "application/json",
73+
body: UpdateProjectOutputSchema,
74+
},
75+
])
76+
.handler(handle),
77+
});

src/packages/server/projects/get.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,21 @@
77
import getPool from "@cocalc/database/pool";
88
import { isValidUUID } from "@cocalc/util/misc";
99

10+
export interface DBProject {
11+
project_id: string;
12+
title?: string;
13+
description?: string;
14+
name?: string;
15+
}
16+
1017
// I may add more fields and more options later...
1118
export default async function getProjects({
1219
account_id,
1320
limit = 50,
1421
}: {
1522
account_id: string;
1623
limit?: number;
17-
}): Promise<{ project_id: string; title?: string; description?: string }[]> {
24+
}): Promise<DBProject[]> {
1825
if (!isValidUUID(account_id)) {
1926
throw Error("account_id must be a UUIDv4");
2027
}
@@ -23,8 +30,8 @@ export default async function getProjects({
2330
}
2431
const pool = getPool();
2532
const { rows } = await pool.query(
26-
`SELECT project_id, title, description FROM projects WHERE DELETED IS NOT true AND users ? $1 AND (users#>>'{${account_id},hide}')::BOOLEAN IS NOT TRUE ORDER BY last_edited DESC LIMIT $2`,
27-
[account_id, limit]
33+
`SELECT project_id, title, description, name FROM projects WHERE DELETED IS NOT true AND users ? $1 AND (users#>>'{${account_id},hide}')::BOOLEAN IS NOT TRUE ORDER BY last_edited DESC LIMIT $2`,
34+
[account_id, limit],
2835
);
2936
return rows;
3037
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* Updates an existing project's name, title, and/or description. May be
2+
restricted such that the query is executed as though by a specific account_id.
3+
4+
This function is simply a wrapper around the userQuery function.
5+
*/
6+
import userQuery from "@cocalc/database/user-query";
7+
8+
import { DBProject } from "./get";
9+
10+
export default async function setProject({
11+
acting_account_id,
12+
project_id,
13+
project_update,
14+
}: {
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;
20+
project_id: string;
21+
project_update: Omit<DBProject, "project_id">;
22+
}): Promise<DBProject | undefined> {
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+
});
38+
}

0 commit comments

Comments
 (0)