- 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]>
97 lines
4.1 KiB
TypeScript
97 lines
4.1 KiB
TypeScript
import userAuth from "@/src/functions/backend/auth/user-auth";
|
|
import ensureCoordinatorBot from "@/src/functions/backend/db/bots/ensure-coordinator-bot";
|
|
import ensureMainComputer from "@/src/functions/backend/db/computers/ensure-main-computer";
|
|
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
|
import saveUser from "@/src/functions/backend/db/users/save-user";
|
|
import logEvent, { grabRequestIP, logError } from "@/src/functions/backend/logger";
|
|
import { SaveUserRequestSchema } from "@/src/schemas/user-schema";
|
|
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
|
import AppError from "@/src/utils/app-error";
|
|
import parseWithSchema from "@/src/utils/parse-with-schema";
|
|
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
|
import canCreateUser from "@/src/utils/rbac/can-create-user";
|
|
import canUpdateUser from "@/src/utils/rbac/can-update-user";
|
|
import parseUserTypes from "@/src/utils/parse-user-types";
|
|
import safeErrorMessage from "@/src/utils/safe-error-message";
|
|
import type {
|
|
APIResponseObject,
|
|
BunextAPIRouteHandler,
|
|
} from "@moduletrace/bunext/types";
|
|
|
|
/**
|
|
* Creates a user (`canCreateUser`) or updates one (`canUpdateUser`):
|
|
* name, email, username, password and user types, each type checked
|
|
* against the assignment matrix. A new user gets their coordinator and main computer
|
|
* straight away, as at signup. Returns the user.
|
|
*/
|
|
export const handler: BunextAPIRouteHandler<APIResponseObject<BUN_SQLITE_COBBLE_USERS_JOIN>> = async ({ req, body }) => {
|
|
try {
|
|
if (req.method != "POST") {
|
|
throw new AppError(`Method not allowed`);
|
|
}
|
|
|
|
const { user, user_types } = await userAuth({ req });
|
|
|
|
if (!user?.logged_in_status || !user.id) {
|
|
return { success: false, msg: `Unauthorized`, logoutUser: true };
|
|
}
|
|
|
|
if (isReadOnlyUser({ user_types: user_types || [] })) {
|
|
throw new AppError(`This user can only read records.`);
|
|
}
|
|
|
|
const data = parseWithSchema({ schema: SaveUserRequestSchema, data: body });
|
|
|
|
const target = data.user_id ? (await grabUsers({ user_id: data.user_id })).singleRes : null;
|
|
|
|
const allowed = data.user_id
|
|
? canUpdateUser({
|
|
actor_user_types: user_types,
|
|
actor_user_id: user.id,
|
|
target_user_id: data.user_id,
|
|
target_user_types: parseUserTypes({ user_types: target?.user_types }).map((user_type) => ({ user_type })),
|
|
update_fields: target && target.email != data.email.trim().toLowerCase() ? ["email"] : [],
|
|
})
|
|
: canCreateUser({ user_types });
|
|
|
|
if (!allowed) {
|
|
throw new AppError(`You can't do that.`);
|
|
}
|
|
const { user_id, created } = await saveUser({ ...data, actor_user_types: user_types || [] });
|
|
|
|
if (created) {
|
|
/**
|
|
* The account exists now; `/admin/bots` creates both on their
|
|
* first visit if this fails.
|
|
*/
|
|
await ensureCoordinatorBot({ user_id })
|
|
.then((coordinator_id) => ensureMainComputer({ user_id, coordinator_id }))
|
|
.catch((error) =>
|
|
logError({
|
|
title: "Coordinator or main computer not created with the user",
|
|
error,
|
|
user_id,
|
|
ip_address: grabRequestIP(req),
|
|
}).catch(() => {}),
|
|
);
|
|
}
|
|
|
|
await logEvent({
|
|
title: created ? "User created" : "User updated",
|
|
category: "auth",
|
|
user_id: user.id,
|
|
plain_text: `User #${user.id} ${created ? "created" : "updated"} user #${user_id}.`,
|
|
metadata: { user_id, user_types: data.user_types, password_changed: Boolean(data.password) },
|
|
ip_address: grabRequestIP(req),
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
msg: created ? `${data.first_name} can sign in now.` : `Saved.`,
|
|
singleRes: (await grabUsers({ user_id })).singleRes,
|
|
};
|
|
} catch (error: unknown) {
|
|
return { success: false, msg: safeErrorMessage({ error }) };
|
|
}
|
|
};
|