Skip to content

Commit a38f73e

Browse files
Merge pull request #7762 from schrodingersket/feature/7665
Account Management API: Added v2 API endpoint to list project collaborators…
2 parents 3023103 + 533b95e commit a38f73e

File tree

6 files changed

+121
-16
lines changed

6 files changed

+121
-16
lines changed

src/packages/next/components/project/project.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Loading from "components/share/loading";
1717
import { Layout } from "components/share/layout";
1818
import A from "components/misc/A";
1919
import { Customize } from "lib/share/customize";
20-
import { User } from "lib/share/types";
20+
import { ProjectCollaborator } from "lib/api/schema/projects/collaborators/list";
2121
import Edit from "./edit";
2222
import editURL from "lib/share/edit-url";
2323
import Markdown from "@cocalc/frontend/editors/slate/static-markdown";
@@ -104,7 +104,7 @@ export default function Project({
104104

105105
function isCollaborator(
106106
account: undefined | { account_id: string },
107-
collaborators: User[]
107+
collaborators: ProjectCollaborator[],
108108
): boolean {
109109
const account_id = account?.account_id;
110110
if (account_id == null) return false;

src/packages/next/components/share/collaborators.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import User from "./user";
2-
import { User as IUser } from "lib/share/types";
2+
import { ProjectCollaborator } from "lib/api/schema/projects/collaborators/list";
33

44
export default function Collaborators({
55
collaborators,
66
}: {
7-
collaborators: IUser[];
7+
collaborators: ProjectCollaborator[];
88
}) {
99
const v: JSX.Element[] = [];
1010
for (const user of collaborators ?? []) {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { z } from "../../../framework";
2+
3+
import { FailedAPIOperationSchema } from "../../common";
4+
5+
import { ProjectIdSchema } from "../common";
6+
7+
export const ProjectCollaboratorSchema = z.object({
8+
account_id: z.string().uuid(),
9+
first_name: z.string(),
10+
last_name: z.string(),
11+
});
12+
13+
export type ProjectCollaborator = z.infer<typeof ProjectCollaboratorSchema>;
14+
15+
// OpenAPI spec
16+
//
17+
export const ListProjectCollaboratorsInputSchema = z
18+
.object({
19+
project_id: ProjectIdSchema,
20+
})
21+
.describe(
22+
`List all collaborators on a project. When executed as an administrator, any project
23+
may be queried; otherwise, this endpoint only returns collaborators on projects for
24+
which the client account is itself a collaborator.`,
25+
);
26+
27+
export const ListProjectCollaboratorsOutputSchema = z.union([
28+
FailedAPIOperationSchema,
29+
z.array(ProjectCollaboratorSchema),
30+
]);
31+
32+
export type ListProjectCollaboratorsInput = z.infer<
33+
typeof ListProjectCollaboratorsInputSchema
34+
>;
35+
export type ListProjectCollaboratorsOutput = z.infer<
36+
typeof ListProjectCollaboratorsOutputSchema
37+
>;

src/packages/next/lib/share/get-collaborators.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,31 @@ Get the collaborators on a given project. Unlisted collaborators are NOT includ
88
*/
99

1010
import getPool from "@cocalc/database/pool";
11-
import { User } from "./types";
1211
import { isUUID } from "./util";
12+
import { ProjectCollaborator } from "../api/schema/projects/collaborators/list";
1313

1414
export default async function getCollaborators(
15-
project_id: string
16-
): Promise<User[]> {
15+
project_id: string,
16+
account_id?: string,
17+
): Promise<ProjectCollaborator[]> {
1718
if (!isUUID(project_id)) {
1819
throw Error("project_id must be a uuid");
1920
}
20-
const pool = getPool('medium');
21+
const pool = getPool("medium");
22+
let subQuery = `SELECT jsonb_object_keys(users) AS account_id FROM projects WHERE project_id=$1`;
23+
24+
const queryParams = [project_id];
25+
26+
if (account_id) {
27+
queryParams.push(account_id);
28+
subQuery += ` AND users ? $${queryParams.length}::TEXT`;
29+
}
30+
2131
const result = await pool.query(
22-
"SELECT accounts.account_id, accounts.first_name, accounts.last_name FROM accounts, (SELECT jsonb_object_keys(users) AS account_id FROM projects WHERE project_id=$1) AS users WHERE accounts.account_id=users.account_id::UUID AND accounts.unlisted IS NOT TRUE",
23-
[project_id]
32+
`SELECT accounts.account_id, accounts.first_name, accounts.last_name FROM accounts, (${subQuery})
33+
AS users WHERE accounts.account_id=users.account_id::UUID
34+
AND accounts.unlisted IS NOT TRUE`,
35+
queryParams,
2436
);
2537
return result.rows;
2638
}

src/packages/next/lib/share/types.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

6-
export interface User {
7-
account_id: string;
8-
first_name: string;
9-
last_name: string;
10-
}
11-
126
export interface PublicPath {
137
id: string;
148
path?: string;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
API endpoint to list collaborators for a particular project.
3+
4+
Permissions checks are performed by the underlying API call and are NOT
5+
executed at this stage.
6+
7+
*/
8+
import userIsInGroup from "@cocalc/server/accounts/is-in-group";
9+
10+
import getAccountId from "lib/account/get-account";
11+
import getParams from "lib/api/get-params";
12+
import { apiRoute, apiRouteOperation } from "lib/api";
13+
import {
14+
ListProjectCollaboratorsInputSchema,
15+
ListProjectCollaboratorsOutputSchema,
16+
} from "lib/api/schema/projects/collaborators/list";
17+
import getCollaborators from "lib/share/get-collaborators";
18+
19+
async function handle(req, res) {
20+
try {
21+
const client_account_id = await getAccountId(req);
22+
const { project_id } = getParams(req);
23+
24+
// Check authentication
25+
//
26+
if (!client_account_id) {
27+
throw Error("must be signed in");
28+
}
29+
30+
// Allow arbitrary project collaborator queries if client is an administrator.
31+
// Otherwise, restrict by client account id.
32+
//
33+
const collaborators = (await userIsInGroup(client_account_id, "admin"))
34+
? await getCollaborators(project_id)
35+
: await getCollaborators(project_id, client_account_id);
36+
37+
res.json(collaborators);
38+
} catch (err) {
39+
res.json({ error: err.message });
40+
}
41+
}
42+
43+
export default apiRoute({
44+
listProjectCollaborators: apiRouteOperation({
45+
method: "POST",
46+
openApiOperation: {
47+
tags: ["Projects", "Admin"],
48+
},
49+
})
50+
.input({
51+
contentType: "application/json",
52+
body: ListProjectCollaboratorsInputSchema,
53+
})
54+
.outputs([
55+
{
56+
status: 200,
57+
contentType: "application/json",
58+
body: ListProjectCollaboratorsOutputSchema,
59+
},
60+
])
61+
.handler(handle),
62+
});

0 commit comments

Comments
 (0)