Add tabbed settings and system shell

This commit is contained in:
2026-07-31 21:30:51 +02:00
parent d663979ebb
commit c36e5e811f
20 changed files with 698 additions and 29 deletions
+72 -1
View File
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
import argon2 from "argon2";
import cookie from "@fastify/cookie";
import rateLimit from "@fastify/rate-limit";
import websocket from "@fastify/websocket";
import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify";
import { z } from "zod";
import type { AppConfig } from "./config.js";
@@ -28,6 +29,8 @@ import {
requestNetworkAction
} from "./network.js";
import { collectAdbStatus } from "./adb.js";
import { openShell } from "./shell.js";
import { systemPowerActionSchema, triggerSystemPower } from "./system.js";
import "./types.js";
const usernameSchema = z
@@ -114,6 +117,7 @@ export async function buildApp(config: AppConfig): Promise<FastifyInstance> {
app.decorateRequest("authUser", null);
await app.register(cookie);
await app.register(rateLimit, { global: false });
await app.register(websocket);
app.addHook("onClose", async () => {
jobRunner.close();
@@ -405,6 +409,37 @@ export async function buildApp(config: AppConfig): Promise<FastifyInstance> {
return readUpdateStatus(config);
});
app.post(
"/api/system/update/check",
{ config: { rateLimit: { max: 6, timeWindow: "10 minutes" } } },
async (request, reply) => {
if (!requireUser(request, reply)) return;
const current = await readUpdateStatus(config);
if (!current.supported) {
return reply.code(503).send({
error: { code: "UPDATES_UNAVAILABLE", message: "Install the system service before checking for updates" }
});
}
if (["checking", "building"].includes(current.state)) {
return reply.code(409).send({ error: { code: "UPDATE_IN_PROGRESS", message: "An update task is already running" } });
}
try {
await triggerSystemUpdate("check");
audit(database, {
actorUserId: request.authUser!.id,
action: "system.update-check",
result: "success",
ipAddress: clientAddress(request)
});
return reply.code(202).send({ queued: true });
} catch {
return reply.code(503).send({
error: { code: "UPDATE_CHECK_FAILED", message: "The update check could not be started" }
});
}
}
);
app.post(
"/api/system/update",
{ config: { rateLimit: { max: 2, timeWindow: "10 minutes" } } },
@@ -421,7 +456,7 @@ export async function buildApp(config: AppConfig): Promise<FastifyInstance> {
}
try {
await triggerSystemUpdate();
await triggerSystemUpdate("install");
audit(database, {
actorUserId: request.authUser!.id,
action: "system.update",
@@ -445,6 +480,42 @@ export async function buildApp(config: AppConfig): Promise<FastifyInstance> {
}
);
app.post(
"/api/system/power",
{ config: { rateLimit: { max: 3, timeWindow: "10 minutes" } } },
async (request, reply) => {
if (!requireUser(request, reply)) return;
const parsed = z.object({ action: systemPowerActionSchema }).safeParse(request.body);
if (!parsed.success) {
return reply.code(400).send({ error: { code: "INVALID_POWER_ACTION", message: "That power action is not available" } });
}
try {
await triggerSystemPower(parsed.data.action);
audit(database, {
actorUserId: request.authUser!.id,
action: `system.${parsed.data.action}`,
result: "success",
ipAddress: clientAddress(request)
});
return reply.code(202).send({ queued: true, action: parsed.data.action });
} catch {
audit(database, {
actorUserId: request.authUser!.id,
action: `system.${parsed.data.action}`,
result: "failure",
ipAddress: clientAddress(request)
});
return reply.code(503).send({
error: { code: "POWER_ACTION_FAILED", message: "The Pi could not schedule that power action" }
});
}
}
);
app.get("/api/system/shell", { websocket: true }, (socket, request) => {
openShell(socket, request, config);
});
app.get("/api/events", async (request, reply) => {
if (!requireUser(request, reply)) return;
reply.hijack();