- Schema: workspaces (+ workspace_bots, workspace_computers), containers, computers, bot_computers, bots.memory, workspaces.system_prompt, and bots_queue as a bun-sqlite isQueue table - Main computer (and its container record) ensured with the coordinator on signup, seed and /admin/bots; the coordinator's access to it is locked - Effective computer access: direct grants plus workspace computers - Full bot prompt: core instructions, workspace prompts, then memory; bots can rewrite their memory and system prompt - Queue helpers on the native BunSQLite.queue API, plus a handler that heartbeats, serializes per computer, recovers after restarts, reports lost jobs, supports cancelling and pushes outcomes over the WebSocket - Enable SQLite foreign keys at startup so schema cascades fire - Prune finished jobs daily Co-Authored-By: Claude Opus 5.5 <[email protected]>
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
import createSuperAdminUser from "@/src/functions/backend/auth/create-super-admin-user";
|
|
import setUserPassword from "@/src/functions/backend/auth/set-user-password";
|
|
import grabHasUsers from "@/src/functions/backend/auth/grab-has-users";
|
|
import loginUser from "@/src/functions/backend/auth/login-user";
|
|
import ensureCoordinatorBot from "@/src/functions/backend/db/bots/ensure-coordinator-bot";
|
|
import ensureMainComputer from "@/src/functions/backend/db/computers/ensure-main-computer";
|
|
import validatePassword from "@/src/utils/validate-password";
|
|
import logEvent, { grabRequestIP, logError } from "@/src/functions/backend/logger";
|
|
import { AppData } from "@/src/data/app-data";
|
|
import rateLimit from "@/src/utils/rate-limit";
|
|
import { SignupRequestSchema } from "@/src/schemas/auth-schema";
|
|
import parseWithSchema from "@/src/utils/parse-with-schema";
|
|
import safeErrorMessage from "@/src/utils/safe-error-message";
|
|
import AppError from "@/src/utils/app-error";
|
|
import type {
|
|
APIResponseObject,
|
|
BunextAPIRouteHandler,
|
|
} from "@moduletrace/bunext/types";
|
|
|
|
/**
|
|
* First-run setup: creates the super admin and their coordinator bot, then
|
|
* logs them in. Refused once
|
|
* any account exists, so signup can only ever happen once per install.
|
|
*/
|
|
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
|
req,
|
|
body,
|
|
}) => {
|
|
try {
|
|
const ip_limit = rateLimit({
|
|
key: `signup:ip:${grabRequestIP(req) || "unknown"}`,
|
|
limit: AppData["PasswordAttemptLimit"],
|
|
window_ms: AppData["RateLimitWindowMinutes"] * 60 * 1000,
|
|
});
|
|
|
|
if (!ip_limit.allowed) {
|
|
throw new AppError(`Too many requests. Please try again later.`);
|
|
}
|
|
|
|
if (await grabHasUsers()) {
|
|
throw new AppError(`This install has already been set up.`);
|
|
}
|
|
|
|
const { signup } = parseWithSchema({
|
|
schema: SignupRequestSchema,
|
|
data: body,
|
|
});
|
|
|
|
const email = signup.email.trim().toLowerCase();
|
|
const username = signup.username.trim().toLowerCase();
|
|
|
|
if (!AppData["UsernameRegex"].test(username)) {
|
|
throw new AppError(
|
|
`Usernames must be 3-32 characters: lowercase letters, numbers, dots, dashes or underscores`,
|
|
);
|
|
}
|
|
|
|
const password_error = validatePassword({ password: signup.password });
|
|
|
|
if (password_error) {
|
|
throw new AppError(password_error);
|
|
}
|
|
|
|
const new_user_id = await createSuperAdminUser({
|
|
first_name: signup.first_name.trim(),
|
|
last_name: signup.last_name?.trim(),
|
|
email,
|
|
username,
|
|
});
|
|
|
|
await setUserPassword({
|
|
user_id: new_user_id,
|
|
password: signup.password,
|
|
});
|
|
|
|
/**
|
|
* The account exists now, so a failure here mustn't fail signup
|
|
* (which is one-shot). `/admin/bots` creates both on the next visit.
|
|
*/
|
|
await ensureCoordinatorBot({ user_id: new_user_id })
|
|
.then((coordinator_id) =>
|
|
ensureMainComputer({ user_id: new_user_id, coordinator_id }),
|
|
)
|
|
.catch((error) =>
|
|
logError({
|
|
title: "Coordinator or main computer not created at signup",
|
|
error,
|
|
user_id: new_user_id,
|
|
ip_address: grabRequestIP(req),
|
|
}).catch(() => {}),
|
|
);
|
|
|
|
await logEvent({
|
|
title: "Super admin created",
|
|
category: "auth",
|
|
user_id: new_user_id,
|
|
plain_text: `User #${new_user_id} created the super admin account.`,
|
|
metadata: {
|
|
user_id: new_user_id,
|
|
},
|
|
ip_address: grabRequestIP(req),
|
|
});
|
|
|
|
return await loginUser({
|
|
user_id: new_user_id,
|
|
req,
|
|
});
|
|
} catch (error: unknown) {
|
|
await logError({
|
|
title: "Signup failed",
|
|
error,
|
|
ip_address: grabRequestIP(req),
|
|
}).catch(() => {});
|
|
|
|
return {
|
|
success: false,
|
|
msg: safeErrorMessage({ error }),
|
|
};
|
|
}
|
|
};
|