Skip to content

Commit 3194d4c

Browse files
Account Management API: Added admin support and documentation to get account projects via /api/v2/projects/get.
1 parent 3b3507f commit 3194d4c

File tree

6 files changed

+136
-39
lines changed

6 files changed

+136
-39
lines changed

src/packages/next/lib/api/schema/accounts/common.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ export const AccountIdSchema = z.string().uuid().describe("Account id.");
44

55
export type AccountId = z.infer<typeof AccountIdSchema>;
66

7+
export const AdminAccountIdSchema = AccountIdSchema.optional().describe(
8+
`**Administrators only**. Optional account id to set name(s) for. If this field is
9+
not provided, it is assumed that this operation pertains to the account id of the
10+
user making the request.`,
11+
);
12+
13+
export type AdminAccountId = z.infer<typeof AdminAccountIdSchema>;
14+
715
export const AccountEmailSchema = z
816
.string()
917
.describe("The account e-mail address.");
Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
11
import { z } from "../../framework";
22

3-
export const ProjectIdSchema = z
4-
.string()
5-
.uuid()
6-
.describe("Project id.");
3+
export const ProjectIdSchema = z.string().uuid().describe("Project id.");
74

85
export type ProjectId = z.infer<typeof ProjectIdSchema>;
6+
7+
export const ProjectTitleSchema = z
8+
.string()
9+
.describe(
10+
"The short title of the project. Should use no special formatting, except hashtags.",
11+
);
12+
13+
export type ProjectTitle = z.infer<typeof ProjectTitleSchema>;
14+
15+
export const ProjectDescriptionSchema = z.string().describe(
16+
`A longer textual description of the project. This can include hashtags and should
17+
be formatted using markdown.`,
18+
);
19+
20+
export type ProjectDescription = z.infer<typeof ProjectDescriptionSchema>;
21+
22+
export const ProjectNameSchema = z.string().describe(
23+
`The optional name of this project. Must be globally unique (up to case) across all
24+
projects with a given *owner*. It can be between 1 and 100 characters from a-z, A-Z,
25+
0-9, period, and dash.`,
26+
);
27+
28+
export type ProjectName = z.infer<typeof ProjectNameSchema>;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { z } from "../../framework";
2+
3+
import { FailedAPIOperationSchema } from "../common";
4+
5+
import { AdminAccountIdSchema } from "../accounts/common";
6+
7+
import { ProjectIdSchema } from "./common";
8+
9+
// OpenAPI spec
10+
//
11+
export const GetAccountProjectsInputSchema = z
12+
.object({
13+
account_id: AdminAccountIdSchema,
14+
limit: z
15+
.number()
16+
.default(50)
17+
.describe("Upper bound on the number of projects to return.")
18+
.nullish(),
19+
})
20+
.describe("Gets projects for a particular account.");
21+
22+
export const GetAccountProjectsOutputSchema = z.union([
23+
FailedAPIOperationSchema,
24+
z
25+
.array(
26+
z.object({
27+
project_id: ProjectIdSchema,
28+
title: ProjectIdSchema,
29+
}),
30+
)
31+
.describe("An array of projects corresponding to a particular account."),
32+
]);
33+
34+
export type GetAccountProjectsInput = z.infer<
35+
typeof GetAccountProjectsInputSchema
36+
>;
37+
export type GetAccountProjectsOutput = z.infer<
38+
typeof GetAccountProjectsOutputSchema
39+
>;

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

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,24 @@ import { z } from "../../framework";
22

33
import { FailedAPIOperationSchema, OkAPIOperationSchema } from "../common";
44

5-
import { ProjectIdSchema } from "./common";
6-
import { AccountIdSchema } from "../accounts/common";
5+
import { AdminAccountIdSchema } from "../accounts/common";
6+
7+
import {
8+
ProjectDescriptionSchema,
9+
ProjectIdSchema,
10+
ProjectNameSchema,
11+
ProjectTitleSchema,
12+
} from "./common";
713

814
// OpenAPI spec
915
//
1016
export const UpdateProjectInputSchema = z
1117
.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(),
18+
account_id: AdminAccountIdSchema,
1619
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(),
20+
title: ProjectTitleSchema.optional(),
21+
description: ProjectDescriptionSchema.optional(),
22+
name: ProjectNameSchema.optional(),
3923
})
4024
.describe(
4125
`Update an existing project's title, name, and/or description. If the API client is an

src/packages/next/pages/api/v2/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export default docsApiRoute({
1111
version: "1.0.0",
1212
summary: `This is the CoCalc HTTP API. To get started, you'll need to
1313
[create an API key](https://doc.cocalc.com/apikeys.html).`,
14-
description: "foo bar",
14+
description: `This is the CoCalc HTTP API. To get started, you'll need to
15+
[create an API key](https://doc.cocalc.com/apikeys.html).`,
1516
},
1617
externalDocs: {
1718
url: "https://doc.cocalc.com",
Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,61 @@
11
/* Get projects that the authenticated user is a collaborator on. */
22

3-
import getAccountId from "lib/account/get-account";
43
import getProjects from "@cocalc/server/projects/get";
4+
import userIsInGroup from "@cocalc/server/accounts/is-in-group";
5+
import getAccountId from "lib/account/get-account";
56
import getParams from "lib/api/get-params";
7+
import { apiRoute, apiRouteOperation } from "lib/api";
68

7-
export default async function handle(req, res) {
8-
const account_id = await getAccountId(req);
9+
import {
10+
GetAccountProjectsInputSchema,
11+
GetAccountProjectsOutputSchema,
12+
} from "lib/api/schema/projects/get";
13+
14+
async function handle(req, res) {
15+
const client_account_id = await getAccountId(req);
916
try {
10-
if (account_id == null) throw Error("must be authenticated");
11-
const { limit } = getParams(req);
12-
res.json(await getProjects({ account_id, limit }));
17+
if (client_account_id == null) {
18+
throw Error("Must be signed in.");
19+
}
20+
21+
const { account_id, limit } = getParams(req);
22+
23+
// User must be an admin to specify account_id field
24+
//
25+
if (account_id && !(await userIsInGroup(client_account_id, "admin"))) {
26+
throw Error(
27+
"The `account_id` field may only be specified by account administrators.",
28+
);
29+
}
30+
31+
res.json(
32+
await getProjects({
33+
account_id: account_id || client_account_id,
34+
limit,
35+
}),
36+
);
1337
} catch (err) {
1438
res.json({ error: err.message });
1539
}
1640
}
41+
42+
export default apiRoute({
43+
get: apiRouteOperation({
44+
method: "POST",
45+
openApiOperation: {
46+
tags: ["Projects", "Admin"],
47+
},
48+
})
49+
.input({
50+
contentType: "application/json",
51+
body: GetAccountProjectsInputSchema,
52+
})
53+
.outputs([
54+
{
55+
status: 200,
56+
contentType: "application/json",
57+
body: GetAccountProjectsOutputSchema,
58+
},
59+
])
60+
.handler(handle),
61+
});

0 commit comments

Comments
 (0)