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]>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import setUserGrant from "@/src/functions/backend/db/access/set-user-grant";
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import { UserGrantRequestSchema } 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 parseUserTypes from "@/src/utils/parse-user-types";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
|
||||
import canUpdateUser from "@/src/utils/rbac/can-update-user";
|
||||
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
type Params = {
|
||||
req: Request;
|
||||
body: unknown;
|
||||
/**
|
||||
* Gives the worker or workspace when true, takes it away when false.
|
||||
*/
|
||||
granted: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* `users/add-access` and `users/remove-access`: gives a user one worker
|
||||
* or workspace, or takes it away, for whoever manages the crew and may
|
||||
* update that user (`canManageCrew`, `canUpdateUser`). Returns the user.
|
||||
*/
|
||||
export default async function handleUserGrant({
|
||||
req,
|
||||
body,
|
||||
granted,
|
||||
}: Params): Promise<APIResponseObject<BUN_SQLITE_COBBLE_USERS_JOIN>> {
|
||||
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 { user_id, kind, id } = parseWithSchema({ schema: UserGrantRequestSchema, data: body });
|
||||
const target = (await grabUsers({ user_id })).singleRes;
|
||||
|
||||
if (!target?.id) {
|
||||
throw new AppError(`There's no such user.`);
|
||||
}
|
||||
|
||||
const allowed =
|
||||
canManageCrew({ user_types }) &&
|
||||
canUpdateUser({
|
||||
actor_user_types: user_types,
|
||||
actor_user_id: user.id,
|
||||
target_user_id: user_id,
|
||||
target_user_types: parseUserTypes({ user_types: target.user_types }).map((user_type) => ({ user_type })),
|
||||
update_fields: [],
|
||||
});
|
||||
|
||||
if (!allowed) {
|
||||
throw new AppError(`You can't do that.`);
|
||||
}
|
||||
|
||||
await setUserGrant({ user_id, kind, id, granted });
|
||||
|
||||
await logEvent({
|
||||
title: granted ? "User access given" : "User access removed",
|
||||
category: "auth",
|
||||
user_id: user.id,
|
||||
plain_text: `User #${user.id} ${granted ? "gave" : "took"} ${kind} #${id} ${granted ? "to" : "from"} user #${user_id}.`,
|
||||
metadata: { user_id, kind, id, granted },
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return { success: true, singleRes: (await grabUsers({ user_id })).singleRes };
|
||||
} catch (error: unknown) {
|
||||
return { success: false, msg: safeErrorMessage({ error }) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
import handleUserGrant from "./(functions)/handle-user-grant";
|
||||
|
||||
/**
|
||||
* Gives a user one worker or workspace (`handleUserGrant`). Returns the user.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject<BUN_SQLITE_COBBLE_USERS_JOIN>> = async ({ req, body }) =>
|
||||
handleUserGrant({ req, body, granted: true });
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
import handleUserGrant from "./(functions)/handle-user-grant";
|
||||
|
||||
/**
|
||||
* Takes one worker or workspace away from a user (`handleUserGrant`). Returns the user.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject<BUN_SQLITE_COBBLE_USERS_JOIN>> = async ({ req, body }) =>
|
||||
handleUserGrant({ req, body, granted: false });
|
||||
@@ -1,5 +1,4 @@
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import setUserAccess from "@/src/functions/backend/db/access/set-user-access";
|
||||
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";
|
||||
@@ -21,10 +20,9 @@ import type {
|
||||
/**
|
||||
* Creates a user (`canCreateUser`) or updates one (`canUpdateUser`):
|
||||
* name, email, username, password, user types (each checked against the
|
||||
* assignment matrix), when one is sent their profile photo, and when sent
|
||||
* the workers assigned to them and the workspaces they're in
|
||||
* (`setUserAccess`). Whoever may create or update users manages the crew
|
||||
* too (super admins and admins). Returns the user.
|
||||
* 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 {
|
||||
@@ -65,10 +63,6 @@ export const handler: BunextAPIRouteHandler<APIResponseObject<BUN_SQLITE_COBBLE_
|
||||
await setUserProfileImage({ user_id, data_url: data.image });
|
||||
}
|
||||
|
||||
if (data.bot_ids || data.workspace_ids) {
|
||||
await setUserAccess({ user_id, bot_ids: data.bot_ids, workspace_ids: data.workspace_ids });
|
||||
}
|
||||
|
||||
await logEvent({
|
||||
title: created ? "User created" : "User updated",
|
||||
category: "auth",
|
||||
@@ -78,8 +72,6 @@ export const handler: BunextAPIRouteHandler<APIResponseObject<BUN_SQLITE_COBBLE_
|
||||
user_id,
|
||||
user_types: data.user_types,
|
||||
password_changed: Boolean(data.password),
|
||||
bot_ids: data.bot_ids,
|
||||
workspace_ids: data.workspace_ids,
|
||||
},
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user