Files
cobble/src/pages/api/admin/users/save-user.ts
T
tbenandClaude Opus 5.5 391943a1c2 Set a user's bots and workspaces in their own groups, saved at once
Take the Bots and Workspaces pickers out of the user form. A user's
settings page lists what they reach in two groups, like a workspace's
Bots group: each row links to its settings and can be taken away, and
a select adds one, saved straight away through users/add-access and
users/remove-access (setUserGrant).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 15:44:59 +01:00

88 lines
3.6 KiB
TypeScript

import userAuth from "@/src/functions/backend/auth/user-auth";
import grabUsers from "@/src/functions/backend/db/users/grab-users";
import saveUser from "@/src/functions/backend/db/users/save-user";
import setUserProfileImage from "@/src/functions/backend/db/users/set-user-profile-image";
import logEvent, { grabRequestIP } 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, user types (each checked against the
* assignment matrix), and when one is sent their profile photo. What they
* reach is set on its own (`users/add-access`, `users/remove-access`).
* 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 (data.image) {
await setUserProfileImage({ user_id, data_url: data.image });
}
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 }) };
}
};