add build profiles
This commit is contained in:
@@ -193,6 +193,18 @@ try { db.run(`ALTER TABLE builds ADD COLUMN build_tag TEXT`); } catch {}
|
||||
try { db.run(`ALTER TABLE builds ADD COLUMN built_by_user_id INTEGER`); } catch {}
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_builds_build_tag ON builds(build_tag);`);
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS build_profiles (
|
||||
user_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
profile_json TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, name)
|
||||
);
|
||||
`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_build_profiles_user_updated ON build_profiles(user_id, updated_at DESC);`);
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS notification_screenshots (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -1460,6 +1472,57 @@ export function getClientBookmark(id: string): boolean {
|
||||
return row?.bookmarked === 1;
|
||||
}
|
||||
|
||||
export interface BuildProfileRecord {
|
||||
userId: number;
|
||||
name: string;
|
||||
profileJson: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export function listBuildProfilesForUser(userId: number): BuildProfileRecord[] {
|
||||
return db
|
||||
.query<any>(
|
||||
`SELECT user_id, name, profile_json, created_at, updated_at
|
||||
FROM build_profiles
|
||||
WHERE user_id = ?
|
||||
ORDER BY updated_at DESC, name ASC`,
|
||||
)
|
||||
.all(userId)
|
||||
.map((row: any) => ({
|
||||
userId: row.user_id,
|
||||
name: row.name,
|
||||
profileJson: row.profile_json,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
}));
|
||||
}
|
||||
|
||||
export function saveBuildProfileForUser(userId: number, name: string, profileJson: string): void {
|
||||
const now = Date.now();
|
||||
db.run(
|
||||
`INSERT INTO build_profiles (user_id, name, profile_json, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, name) DO UPDATE SET
|
||||
profile_json = excluded.profile_json,
|
||||
updated_at = excluded.updated_at`,
|
||||
userId,
|
||||
name,
|
||||
profileJson,
|
||||
now,
|
||||
now,
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteBuildProfileForUser(userId: number, name: string): boolean {
|
||||
const result = db.run(
|
||||
`DELETE FROM build_profiles WHERE user_id = ? AND name = ?`,
|
||||
userId,
|
||||
name,
|
||||
);
|
||||
return ((result as any)?.changes || 0) > 0;
|
||||
}
|
||||
|
||||
export interface BuildRecord {
|
||||
id: string;
|
||||
status: string;
|
||||
|
||||
@@ -4,7 +4,16 @@ import { authenticateRequest } from "../../auth";
|
||||
import { AuditAction, logAudit } from "../../auditLog";
|
||||
import * as buildManager from "../../build/buildManager";
|
||||
import * as clientManager from "../../clientManager";
|
||||
import { deleteBuild, getAllBuilds, getBuild, countBuildsForUser, getOldestBuildForUser } from "../../db";
|
||||
import {
|
||||
deleteBuild,
|
||||
getAllBuilds,
|
||||
getBuild,
|
||||
countBuildsForUser,
|
||||
getOldestBuildForUser,
|
||||
listBuildProfilesForUser,
|
||||
saveBuildProfileForUser,
|
||||
deleteBuildProfileForUser,
|
||||
} from "../../db";
|
||||
import { encodeMessage } from "../../protocol";
|
||||
import { metrics } from "../../metrics";
|
||||
import { requirePermission } from "../../rbac";
|
||||
@@ -380,6 +389,90 @@ export async function handleBuildRoutes(
|
||||
return Response.json({ builds });
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/api/build/profiles") {
|
||||
requirePermission(user, "clients:build");
|
||||
|
||||
const profiles = listBuildProfilesForUser(user.userId).map((p) => {
|
||||
let parsedConfig: any = {};
|
||||
try {
|
||||
parsedConfig = JSON.parse(p.profileJson);
|
||||
} catch {
|
||||
parsedConfig = {};
|
||||
}
|
||||
return {
|
||||
name: p.name,
|
||||
config: parsedConfig,
|
||||
createdAt: p.createdAt,
|
||||
updatedAt: p.updatedAt,
|
||||
};
|
||||
});
|
||||
return Response.json({ profiles });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/build/profiles") {
|
||||
requirePermission(user, "clients:build");
|
||||
|
||||
const body = await req.json();
|
||||
const name = typeof body?.name === "string" ? body.name.trim() : "";
|
||||
const config = body?.config;
|
||||
|
||||
if (!name || name.length > 64 || !/^[A-Za-z0-9 _.-]+$/.test(name)) {
|
||||
return Response.json(
|
||||
{ error: "Invalid profile name (1-64 chars, letters/numbers/space/._-)" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
||||
return Response.json({ error: "Invalid profile config" }, { status: 400 });
|
||||
}
|
||||
|
||||
const MAX_PROFILES_PER_USER = 50;
|
||||
const existingProfiles = listBuildProfilesForUser(user.userId);
|
||||
const hasExistingWithSameName = existingProfiles.some((p) => p.name === name);
|
||||
if (!hasExistingWithSameName && existingProfiles.length >= MAX_PROFILES_PER_USER) {
|
||||
return Response.json(
|
||||
{ error: `Profile limit reached (${MAX_PROFILES_PER_USER} max)` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
let profileJson = "{}";
|
||||
try {
|
||||
profileJson = JSON.stringify(config);
|
||||
} catch {
|
||||
return Response.json({ error: "Profile config must be JSON serializable" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (profileJson.length > 512 * 1024) {
|
||||
return Response.json({ error: "Profile config too large (max 512KB)" }, { status: 400 });
|
||||
}
|
||||
|
||||
saveBuildProfileForUser(user.userId, name, profileJson);
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
|
||||
if (req.method === "DELETE" && url.pathname.match(/^\/api\/build\/profiles\/.+$/)) {
|
||||
requirePermission(user, "clients:build");
|
||||
|
||||
const encodedName = url.pathname.split("/api/build/profiles/")[1] || "";
|
||||
let name = "";
|
||||
try {
|
||||
name = decodeURIComponent(encodedName).trim();
|
||||
} catch {
|
||||
return Response.json({ error: "Invalid profile name" }, { status: 400 });
|
||||
}
|
||||
if (!name) {
|
||||
return Response.json({ error: "Invalid profile name" }, { status: 400 });
|
||||
}
|
||||
|
||||
const deleted = deleteBuildProfileForUser(user.userId, name);
|
||||
if (!deleted) {
|
||||
return Response.json({ error: "Profile not found" }, { status: 404 });
|
||||
}
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
|
||||
if (req.method === "DELETE" && url.pathname.match(/^\/api\/build\/(.+)\/delete$/)) {
|
||||
requirePermission(user, "clients:build");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user