fix(control): allow login when HTTP server is stopped

Mount auth routes on the control plane and proxy /api/auth to :8600 in
dev:pm2 so the UI can authenticate while the HTTP API process is down.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-20 11:00:47 +07:00
co-authored by Cursor
parent 21a31b0b54
commit eb94a2f669
5 changed files with 35 additions and 13 deletions
+9 -1
View File
@@ -4,7 +4,7 @@ import cors from "@fastify/cors";
import jwt from "@fastify/jwt"; import jwt from "@fastify/jwt";
import { migrate, db } from "./db.js"; import { migrate, db } from "./db.js";
import { log, enableLogPersistence, flushLogs } from "./logger.js"; import { log, enableLogPersistence, flushLogs } from "./logger.js";
import { COOKIE } from "./src/api/auth.js"; import authPlugin, { addApiAuthGuard, COOKIE } from "./src/api/auth.js";
import { import {
clearRestartNeeded, clearRestartNeeded,
bumpGeneration, bumpGeneration,
@@ -124,6 +124,14 @@ server.decorate("requireAdmin", async function requireAdmin(req, reply) {
} }
}); });
await server.register(
async (api) => {
addApiAuthGuard(api, server);
await api.register(authPlugin);
},
{ prefix: "/api" },
);
/** /**
* @param {number} timeoutMs * @param {number} timeoutMs
* @param {string} lockToken * @param {string} lockToken
+1 -1
View File
@@ -108,7 +108,7 @@ run(
["--filter", "@jerapah-flow/web", "dev"], ["--filter", "@jerapah-flow/web", "dev"],
{ {
env: { env: {
// vite.config reads nothing; port is set in vite.config.js JFLOW_AUTH_PROXY: "http://127.0.0.1:8600",
}, },
}, },
); );
+18
View File
@@ -18,6 +18,24 @@ export function cookieOpts() {
}; };
} }
/**
* Require JWT for /api routes except bootstrap, login, and register.
* @param {import("fastify").FastifyInstance} api
* @param {import("fastify").FastifyInstance} root
*/
export function addApiAuthGuard(api, root) {
api.addHook("onRequest", async (req, reply) => {
const raw = (req.url || "").split("?")[0];
const stripped = raw.replace(/^\/api/, "") || "/";
const routeUrl = req.routeOptions?.url || stripped;
const open =
OPEN_API_ROUTES.has(`${req.method} ${routeUrl}`) ||
OPEN_API_ROUTES.has(`${req.method} ${stripped}`);
if (open) return;
await root.authenticate(req, reply);
});
}
export function validateCredentials(username, password) { export function validateCredentials(username, password) {
if (!/^[A-Za-z0-9_]{3,32}$/.test(username)) { if (!/^[A-Za-z0-9_]{3,32}$/.test(username)) {
return "username must be 3-32 letters, numbers, or underscore"; return "username must be 3-32 letters, numbers, or underscore";
+2 -11
View File
@@ -8,7 +8,7 @@ import { migrate, db } from "./db.js";
import { log, enableLogPersistence, flushLogs } from "./logger.js"; import { log, enableLogPersistence, flushLogs } from "./logger.js";
import * as store from "./store.js"; import * as store from "./store.js";
import { createRegistry } from "./registry.js"; import { createRegistry } from "./registry.js";
import { COOKIE, OPEN_API_ROUTES } from "./src/api/auth.js"; import { addApiAuthGuard, COOKIE } from "./src/api/auth.js";
import authPlugin from "./src/api/auth.js"; import authPlugin from "./src/api/auth.js";
import usersPlugin from "./src/api/users.js"; import usersPlugin from "./src/api/users.js";
import scriptsPluginFactory from "./src/api/scripts.js"; import scriptsPluginFactory from "./src/api/scripts.js";
@@ -159,16 +159,7 @@ export async function startApp(opts = {}) {
if (runApi) { if (runApi) {
await server.register( await server.register(
async (api) => { async (api) => {
api.addHook("onRequest", async (req, reply) => { addApiAuthGuard(api, server);
const raw = (req.url || "").split("?")[0];
const stripped = raw.replace(/^\/api/, "") || "/";
const routeUrl = req.routeOptions?.url || stripped;
const open =
OPEN_API_ROUTES.has(`${req.method} ${routeUrl}`) ||
OPEN_API_ROUTES.has(`${req.method} ${stripped}`);
if (open) return;
await server.authenticate(req, reply);
});
await api.register(authPlugin); await api.register(authPlugin);
await api.register(usersPlugin); await api.register(usersPlugin);
await api.register(secretsPlugin); await api.register(secretsPlugin);
+5
View File
@@ -2,11 +2,16 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
/** In dev:pm2, control (:8600) serves auth so login works when HTTP is stopped. */
const authProxyTarget =
process.env.JFLOW_AUTH_PROXY ?? "http://127.0.0.1:8700";
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
server: { server: {
port: 8500, port: 8500,
proxy: { proxy: {
"/api/auth": { target: authProxyTarget, changeOrigin: true },
"/api": { target: "http://127.0.0.1:8700", changeOrigin: true }, "/api": { target: "http://127.0.0.1:8700", changeOrigin: true },
"/admin": { target: "http://127.0.0.1:8700", changeOrigin: true }, "/admin": { target: "http://127.0.0.1:8700", changeOrigin: true },
"/u": { target: "http://127.0.0.1:8700", changeOrigin: true }, "/u": { target: "http://127.0.0.1:8700", changeOrigin: true },