Skip to content

Account Management API: Added v2 API endpoint to delete/restore projects… #7815

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/packages/next/lib/api/schema/projects/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { z } from "../../framework";

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

import { ProjectIdSchema } from "./common";

// OpenAPI spec
//
export const DeleteProjectInputSchema = z
.object({
project_id: ProjectIdSchema,
})
.describe(
`Deletes a specific project. This causes three operations to occur in succession.
Firstly, all project licenses associated with the project are removed. Next, the
project is stopped. Finally, the project's \`delete\` flag in the database is
set, which removes it from the user interface. This operation may be reversed by
restoring the project via the API, with the proviso that all information about
applied project licenses is lost in the delete operation.`,
);

export const DeleteProjectOutputSchema = z.union([
FailedAPIOperationSchema,
OkAPIOperationSchema,
]);

export type DeleteProjectInput = z.infer<typeof DeleteProjectInputSchema>;
export type DeleteProjectOutput = z.infer<typeof DeleteProjectOutputSchema>;
26 changes: 26 additions & 0 deletions src/packages/next/lib/api/schema/projects/restore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { z } from "../../framework";

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

import { ProjectIdSchema } from "./common";

// OpenAPI spec
//
export const RestoreProjectInputSchema = z
.object({
project_id: ProjectIdSchema,
})
.describe(
`Restores a specific project from its deleted state, which clears the project's
\`delete\` flag in the database and restores it to the user interface. Note that any
previously applied project licenses must be re-applied to the project upon
restoration.`,
);

export const RestoreProjectOutputSchema = z.union([
FailedAPIOperationSchema,
OkAPIOperationSchema,
]);

export type RestoreProjectInput = z.infer<typeof RestoreProjectInputSchema>;
export type RestoreProjectOutput = z.infer<typeof RestoreProjectOutputSchema>;
88 changes: 88 additions & 0 deletions src/packages/next/pages/api/v2/projects/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
API endpoint to delete a project, which sets the "delete" flag to `true` in the database.
*/
import isCollaborator from "@cocalc/server/projects/is-collaborator";
import userIsInGroup from "@cocalc/server/accounts/is-in-group";
import removeAllLicensesFromProject from "@cocalc/server/licenses/remove-all-from-project";
import { getProject } from "@cocalc/server/projects/control";
import userQuery from "@cocalc/database/user-query";
import { isValidUUID } from "@cocalc/util/misc";

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 {
DeleteProjectInputSchema,
DeleteProjectOutputSchema,
} from "lib/api/schema/projects/delete";

async function handle(req, res) {
const { project_id } = getParams(req);
const account_id = await getAccountId(req);

try {
if (!isValidUUID(project_id)) {
throw Error("project_id must be a valid uuid");
}
if (!account_id) {
throw Error("must be signed in");
}

// If client is not an administrator, they must be a project collaborator in order to
// delete a project.
if (
!(await userIsInGroup(account_id, "admin")) &&
!(await isCollaborator({ account_id, project_id }))
) {
throw Error("must be an owner to delete a project");
}

// Remove all project licenses
//
await removeAllLicensesFromProject({ project_id });

// Stop project
//
const project = getProject(project_id);
await project.stop();

// Set "deleted" flag. We do this last to ensure that the project is not consuming any
// resources while it is in the deleted state.
//
await userQuery({
account_id,
query: {
projects: {
project_id,
deleted: true,
},
},
});

res.json(OkStatus);
} catch (err) {
res.json({ error: err.message });
}
}

export default apiRoute({
deleteProject: apiRouteOperation({
method: "POST",
openApiOperation: {
tags: ["Projects", "Admin"],
},
})
.input({
contentType: "application/json",
body: DeleteProjectInputSchema,
})
.outputs([
{
status: 200,
contentType: "application/json",
body: DeleteProjectOutputSchema,
},
])
.handler(handle),
});
2 changes: 1 addition & 1 deletion src/packages/next/pages/api/v2/projects/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async function handle(req, res) {
}

export default apiRoute({
get: apiRouteOperation({
getProject: apiRouteOperation({
method: "POST",
openApiOperation: {
tags: ["Projects", "Admin"],
Expand Down
75 changes: 75 additions & 0 deletions src/packages/next/pages/api/v2/projects/restore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
API endpoint to restore a deleted a project, which sets the "delete" flag to `false` in
the database.
*/
import { isValidUUID } from "@cocalc/util/misc";
import isCollaborator from "@cocalc/server/projects/is-collaborator";
import userIsInGroup from "@cocalc/server/accounts/is-in-group";
import userQuery from "@cocalc/database/user-query";

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 {
RestoreProjectInputSchema,
RestoreProjectOutputSchema,
} from "lib/api/schema/projects/restore";

async function handle(req, res) {
const { project_id } = getParams(req);
const account_id = await getAccountId(req);

try {
if (!isValidUUID(project_id)) {
throw Error("project_id must be a valid uuid");
}
if (!account_id) {
throw Error("must be signed in");
}

// If client is not an administrator, they must be a project collaborator in order to
// restore a project.
if (
!(await userIsInGroup(account_id, "admin")) &&
!(await isCollaborator({ account_id, project_id }))
) {
throw Error("must be an owner to restore a project");
}

await userQuery({
account_id,
query: {
projects: {
project_id,
deleted: false,
},
},
});

res.json(OkStatus);
} catch (err) {
res.json({ error: err.message });
}
}

export default apiRoute({
restoreProject: apiRouteOperation({
method: "POST",
openApiOperation: {
tags: ["Projects", "Admin"],
},
})
.input({
contentType: "application/json",
body: RestoreProjectInputSchema,
})
.outputs([
{
status: 200,
contentType: "application/json",
body: RestoreProjectOutputSchema,
},
])
.handler(handle),
});
2 changes: 1 addition & 1 deletion src/packages/next/pages/api/v2/projects/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async function handle(req, res) {
}

export default apiRoute({
start: apiRouteOperation({
startProject: apiRouteOperation({
method: "POST",
openApiOperation: {
tags: ["Projects"],
Expand Down
2 changes: 1 addition & 1 deletion src/packages/next/pages/api/v2/projects/stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async function handle(req, res) {
}

export default apiRoute({
stop: apiRouteOperation({
stopProject: apiRouteOperation({
method: "POST",
openApiOperation: {
tags: ["Projects"],
Expand Down
2 changes: 1 addition & 1 deletion src/packages/next/pages/api/v2/projects/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async function get(req) {
}

export default apiRoute({
update: apiRouteOperation({
updateProject: apiRouteOperation({
method: "POST",
openApiOperation: {
tags: ["Projects", "Admin"],
Expand Down
22 changes: 22 additions & 0 deletions src/packages/server/licenses/remove-all-from-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import getPool, { PoolClient } from "@cocalc/database/pool";

interface Options {
project_id: string;
client?: PoolClient;
}

// Set the site_license field of the entry in the PostgreSQL projects table with given
// project_id to {}. The site_license field is JSONB.
//
export default async function removeAllLicensesFromProject({
project_id,
client,
}: Options) {
const pool = client ?? getPool();
await pool.query(
`
UPDATE projects SET site_license = '{}'::JSONB WHERE project_id = $1
`,
[project_id],
);
}