Let a bot and a workspace set their own envs
Envs had three scopes, general, workspace and computer. A bot had no
scope of its own, so the only way to give one worker a key was to set it
on a computer it happened to share.
The envs table gets a bot_id, and resolution now layers bot over
computer over workspace over general, so a bot's own key wins wherever it
runs. Every computer call site passes the run's bot_id, and grabRunEnvs
does the same for the {{env.KEY}} a custom tool or an MCP server's headers
read.
A bot's and a workspace's settings page can now edit their own envs, in
place, through the same save-env and delete-env routes: sending `scoped`
makes them answer with just that scope instead of the whole install's
overview. Super admins only, like the Environment page.
This commit is contained in:
@@ -3,9 +3,10 @@ import type { BUN_SQLITE_COBBLE_ENVS } from "@/db/types/db";
|
||||
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
|
||||
import deleteEnv from "@/src/functions/backend/db/envs/delete-env";
|
||||
import grabEnvsOverview from "@/src/functions/backend/db/envs/grab-envs-overview";
|
||||
import grabScopedEnvs from "@/src/functions/backend/db/envs/grab-scoped-envs";
|
||||
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import { EnvIdRequestSchema } from "@/src/schemas/env-schema";
|
||||
import type { EnvsOverviewPayload } from "@/src/types";
|
||||
import type { EnvsMutationResult } from "@/src/types";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
@@ -15,10 +16,12 @@ import type {
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
/**
|
||||
* Super admin only: removes an env.
|
||||
* Super admin only: removes an env. Answers with the fresh page data, the
|
||||
* Environment overview or, from a bot's or workspace's settings page
|
||||
* (`scoped`), just that scope's envs.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<
|
||||
APIResponseObject<EnvsOverviewPayload>
|
||||
APIResponseObject<EnvsMutationResult>
|
||||
> = async ({ req, body }) => {
|
||||
try {
|
||||
if (req.method != "POST") {
|
||||
@@ -31,11 +34,14 @@ export const handler: BunextAPIRouteHandler<
|
||||
throw new AppError(`Only a super admin can change the environment.`);
|
||||
}
|
||||
|
||||
const { env_id } = parseWithSchema({ schema: EnvIdRequestSchema, data: body });
|
||||
const { env_id, scoped } = parseWithSchema({
|
||||
schema: EnvIdRequestSchema,
|
||||
data: body,
|
||||
});
|
||||
|
||||
const env = (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_ENVS>({
|
||||
sql: `SELECT key, paradigm FROM envs WHERE id = ? LIMIT 1`,
|
||||
sql: `SELECT key, paradigm, workspace_id, bot_id FROM envs WHERE id = ? LIMIT 1`,
|
||||
values: [env_id],
|
||||
})
|
||||
).payload?.[0];
|
||||
@@ -49,14 +55,33 @@ export const handler: BunextAPIRouteHandler<
|
||||
category: "system",
|
||||
user_id: user.id,
|
||||
plain_text: `User #${user.id} removed ${env.key} (${env.paradigm}).`,
|
||||
metadata: { env_id, key: env.key },
|
||||
metadata: { env_id, key: env.key, paradigm: env.paradigm },
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
if (scoped && (env.paradigm == "bot" || env.paradigm == "workspace")) {
|
||||
const owner_id = env.paradigm == "bot" ? env.bot_id : env.workspace_id;
|
||||
|
||||
if (owner_id) {
|
||||
return {
|
||||
success: true,
|
||||
msg: `Removed ${env.key}.`,
|
||||
singleRes: {
|
||||
scoped: await grabScopedEnvs({
|
||||
paradigm: env.paradigm,
|
||||
owner_id,
|
||||
user_id: Number(user.id),
|
||||
can_manage: true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
msg: `Removed ${env.key}.`,
|
||||
singleRes: await grabEnvsOverview({ user_id: Number(user.id) }),
|
||||
singleRes: { overview: await grabEnvsOverview({ user_id: Number(user.id) }) },
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return { success: false, msg: safeErrorMessage({ error }) };
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
|
||||
import grabEnvsOverview from "@/src/functions/backend/db/envs/grab-envs-overview";
|
||||
import grabScopedEnvs from "@/src/functions/backend/db/envs/grab-scoped-envs";
|
||||
import saveEnv from "@/src/functions/backend/db/envs/save-env";
|
||||
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import { SaveEnvRequestSchema } from "@/src/schemas/env-schema";
|
||||
import type { EnvsOverviewPayload } from "@/src/types";
|
||||
import { SaveEnvRequestSchema, type SaveEnvRequest } from "@/src/schemas/env-schema";
|
||||
import type { EnvsMutationResult } from "@/src/types";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
@@ -14,9 +15,12 @@ import type {
|
||||
|
||||
/**
|
||||
* Super admin only: adds or edits an env. Logged by key, never by value.
|
||||
*
|
||||
* Answers with the fresh page data: the Environment overview or, from a
|
||||
* bot's or workspace's settings page (`scoped`), just that scope's envs.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<
|
||||
APIResponseObject<EnvsOverviewPayload>
|
||||
APIResponseObject<EnvsMutationResult>
|
||||
> = async ({ req, body }) => {
|
||||
try {
|
||||
if (req.method != "POST") {
|
||||
@@ -33,21 +37,66 @@ export const handler: BunextAPIRouteHandler<
|
||||
|
||||
await saveEnv({ ...env, user_id: Number(user.id) });
|
||||
|
||||
const scope_id =
|
||||
env.paradigm == "workspace"
|
||||
? env.workspace_id
|
||||
: env.paradigm == "bot"
|
||||
? env.bot_id
|
||||
: env.paradigm == "computer"
|
||||
? env.computer_id
|
||||
: null;
|
||||
|
||||
await logEvent({
|
||||
title: env.env_id ? "Env changed" : "Env added",
|
||||
category: "system",
|
||||
user_id: user.id,
|
||||
plain_text: `User #${user.id} ${env.env_id ? "changed" : "added"} ${env.key} (${env.paradigm}${env.workspace_id ? ` #${env.workspace_id}` : env.computer_id ? ` #${env.computer_id}` : ""}).`,
|
||||
metadata: { key: env.key, paradigm: env.paradigm, workspace_id: env.workspace_id, computer_id: env.computer_id },
|
||||
plain_text: `User #${user.id} ${env.env_id ? "changed" : "added"} ${env.key} (${env.paradigm}${scope_id ? ` #${scope_id}` : ""}).`,
|
||||
metadata: {
|
||||
key: env.key,
|
||||
paradigm: env.paradigm,
|
||||
workspace_id: env.workspace_id,
|
||||
computer_id: env.computer_id,
|
||||
bot_id: env.bot_id,
|
||||
},
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
msg: `Saved ${env.key.trim()}.`,
|
||||
singleRes: await grabEnvsOverview({ user_id: Number(user.id) }),
|
||||
singleRes: await mutationResult({ env, scoped: env.scoped, user_id: Number(user.id) }),
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return { success: false, msg: safeErrorMessage({ error }) };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* What the page is showing after the change.
|
||||
*/
|
||||
async function mutationResult({
|
||||
env,
|
||||
scoped,
|
||||
user_id,
|
||||
}: {
|
||||
env: SaveEnvRequest;
|
||||
scoped?: boolean;
|
||||
user_id: number;
|
||||
}): Promise<EnvsMutationResult> {
|
||||
if (scoped && (env.paradigm == "bot" || env.paradigm == "workspace")) {
|
||||
const owner_id = env.paradigm == "bot" ? env.bot_id : env.workspace_id;
|
||||
|
||||
if (owner_id) {
|
||||
return {
|
||||
scoped: await grabScopedEnvs({
|
||||
paradigm: env.paradigm,
|
||||
owner_id,
|
||||
user_id,
|
||||
can_manage: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { overview: await grabEnvsOverview({ user_id }) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user