- Computers get CPUs and memory (default 1 CPU, 2 GB), set at creation and editable; new resources stop, remove and recreate the container, its home dir kept. Every computer container has a docker health check and stays "creating" until it passes; only healthy computers are reached, by hostname, else by their IP on the network (dev outside docker). - Docker is required: the app creates the cobble network on startup and exits without docker. - Production container: deploy/run-app-container.sh mounts .env, the data dir and the docker socket; the database (DB_DIR), media, computers' homes and bun-sqlite's live schema now live on the data volume. - Settings pages for bots, workspaces, computers and users, sharing list components; bots can be deleted; users are created with a password and confirmation, edited and deleted through RBAC-gated APIs. - User types trimmed to super_admin, admin, member, read_only and revoked; admins manage members. UserAvatar shows profile photos from the user row. Co-Authored-By: Claude Opus 5.5 <[email protected]>
99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
import BunSQLite from "@moduletrace/bun-sqlite";
|
|
import type { BUN_SQLITE_COBBLE_CONTAINERS } from "@/db/types/db";
|
|
import deleteMedia from "@/src/functions/backend/db/media/delete-media";
|
|
import removeDockerContainer from "@/src/functions/backend/docker/remove-docker-container";
|
|
import AppError from "@/src/utils/app-error";
|
|
import type { BUN_SQLITE_COBBLE_USER_TYPES } from "@/db/types/db";
|
|
import parseUserTypes from "@/src/utils/parse-user-types";
|
|
import canRevokeUser from "@/src/utils/rbac/can-revoke-user";
|
|
import grabUsers from "./grab-users";
|
|
|
|
type Params = {
|
|
/**
|
|
* Who is deleting: never themselves.
|
|
*/
|
|
actor_user_id: number | string;
|
|
/**
|
|
* Whoever may revoke a user may delete them (`canRevokeUser`).
|
|
*/
|
|
actor_user_types: BUN_SQLITE_COBBLE_USER_TYPES[];
|
|
user_id: number | string;
|
|
};
|
|
|
|
/**
|
|
* Deletes a user with everything that's theirs (bots, workspaces,
|
|
* conversations, computers, user types, password, media and its files):
|
|
* their computers' containers are removed first, their home directories
|
|
* stay on disk. Their audit log entries stay too. The super admin can't be
|
|
* deleted, and nobody deletes themselves.
|
|
*/
|
|
export default async function deleteUser({ actor_user_id, actor_user_types, user_id }: Params) {
|
|
if (String(actor_user_id) == String(user_id)) {
|
|
throw new AppError(`You can't delete yourself.`);
|
|
}
|
|
|
|
const user = (await grabUsers({ user_id })).singleRes;
|
|
|
|
if (!user?.id) {
|
|
throw new AppError(`User not found.`);
|
|
}
|
|
|
|
if (parseUserTypes({ user_types: user.user_types }).includes("super_admin")) {
|
|
throw new AppError(`The super admin can't be deleted.`);
|
|
}
|
|
|
|
const target_user_types = parseUserTypes({ user_types: user.user_types }).map((user_type) => ({ user_type }));
|
|
|
|
if (!canRevokeUser({ user_types: actor_user_types, target_user_types })) {
|
|
throw new AppError(`You can't delete ${user.first_name || "this user"}.`);
|
|
}
|
|
|
|
const containers =
|
|
(
|
|
await BunSQLite.sql<BUN_SQLITE_COBBLE_CONTAINERS>({
|
|
sql: `SELECT co.* FROM containers co JOIN computers c ON c.container_id = co.id WHERE c.user_id = ?`,
|
|
values: [user.id],
|
|
})
|
|
).payload || [];
|
|
|
|
for (const container of containers) {
|
|
if (container.docker_id || container.name) {
|
|
await removeDockerContainer({ container: container.docker_id || container.name! });
|
|
}
|
|
}
|
|
|
|
const media_ids =
|
|
(
|
|
await BunSQLite.sql<{ id: number }>({
|
|
sql: `SELECT id FROM media WHERE user_id = ?`,
|
|
values: [user.id],
|
|
})
|
|
).payload?.map((media) => media.id) || [];
|
|
|
|
if (media_ids.length) {
|
|
await deleteMedia({ ids: media_ids, user_id: user.id });
|
|
}
|
|
|
|
const res = await BunSQLite.sql({ sql: `DELETE FROM users WHERE id = ?`, values: [user.id] });
|
|
|
|
if (!res.success) {
|
|
throw new Error(`Couldn't delete the user: ${res.error}`);
|
|
}
|
|
|
|
/**
|
|
* These point at the user without a foreign key, so nothing cascades.
|
|
*/
|
|
for (const table of ["user_types", "user_passwords", "sso_login_codes"]) {
|
|
await BunSQLite.sql({ sql: `DELETE FROM ${table} WHERE user_id = ?`, values: [user.id] });
|
|
}
|
|
|
|
if (containers.length) {
|
|
await BunSQLite.sql({
|
|
sql: `DELETE FROM containers WHERE id IN (${containers.map(() => "?").join(", ")})`,
|
|
values: containers.map((container) => container.id!),
|
|
});
|
|
}
|
|
|
|
return user;
|
|
}
|