optimize docker, more rate limiting, remove not needed shit in build-prod-package.
This commit is contained in:
@@ -111,6 +111,7 @@ RUN if [ -f dist-clients/BackstageCapture.x64.dll ]; then \
|
|||||||
|
|
||||||
# Tailwind CSS, vendored frontend assets, and bundled Bun server runtime.
|
# Tailwind CSS, vendored frontend assets, and bundled Bun server runtime.
|
||||||
RUN bun run build \
|
RUN bun run build \
|
||||||
|
&& bun run minify \
|
||||||
&& test -s ./public/assets/tailwind.css \
|
&& test -s ./public/assets/tailwind.css \
|
||||||
&& test -d ./public/vendor/fontawesome \
|
&& test -d ./public/vendor/fontawesome \
|
||||||
&& test -s ./dist/index.js \
|
&& test -s ./dist/index.js \
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const htmlOpts = {
|
|||||||
|
|
||||||
type Stats = { js: number; css: number; html: number; savedBytes: number };
|
type Stats = { js: number; css: number; html: number; savedBytes: number };
|
||||||
const stats: Stats = { js: 0, css: 0, html: 0, savedBytes: 0 };
|
const stats: Stats = { js: 0, css: 0, html: 0, savedBytes: 0 };
|
||||||
|
let failures = 0;
|
||||||
|
|
||||||
async function collectFiles(dir: string, exts: Set<string>): Promise<string[]> {
|
async function collectFiles(dir: string, exts: Set<string>): Promise<string[]> {
|
||||||
const results: string[] = [];
|
const results: string[] = [];
|
||||||
@@ -94,11 +95,25 @@ console.log(`Minifying ${jsFiles.length} JS, ${cssFiles.length} CSS, ${htmlFiles
|
|||||||
|
|
||||||
// Process in parallel batches
|
// Process in parallel batches
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
...jsFiles.map((f) => minifyJS(f).catch((e) => console.error(`JS error ${f}: ${e.message}`))),
|
...jsFiles.map((f) => minifyJS(f).catch((e) => {
|
||||||
...cssFiles.map((f) => minifyCSS(f).catch((e) => console.error(`CSS error ${f}: ${e.message}`))),
|
failures++;
|
||||||
...htmlFiles.map((f) => minifyHTML(f).catch((e) => console.error(`HTML error ${f}: ${e.message}`))),
|
console.error(`JS error ${f}: ${e.message}`);
|
||||||
|
})),
|
||||||
|
...cssFiles.map((f) => minifyCSS(f).catch((e) => {
|
||||||
|
failures++;
|
||||||
|
console.error(`CSS error ${f}: ${e.message}`);
|
||||||
|
})),
|
||||||
|
...htmlFiles.map((f) => minifyHTML(f).catch((e) => {
|
||||||
|
failures++;
|
||||||
|
console.error(`HTML error ${f}: ${e.message}`);
|
||||||
|
})),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Done: ${stats.js} JS, ${stats.css} CSS, ${stats.html} HTML — saved ${(stats.savedBytes / 1024).toFixed(1)} KB`,
|
`Done: ${stats.js} JS, ${stats.css} CSS, ${stats.html} HTML — saved ${(stats.savedBytes / 1024).toFixed(1)} KB`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (failures > 0) {
|
||||||
|
console.error(`Minification failed for ${failures} file(s).`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -680,7 +680,10 @@ async function startServer() {
|
|||||||
requestIP: (srv as any).requestIP,
|
requestIP: (srv as any).requestIP,
|
||||||
}),
|
}),
|
||||||
(req, url) => handleAssetsRoutes(req, url, routeDeps.assets),
|
(req, url) => handleAssetsRoutes(req, url, routeDeps.assets),
|
||||||
(req, url) => handlePageRoutes(req, url, routeDeps.page),
|
(req, url, srv) => handlePageRoutes(req, url, {
|
||||||
|
...routeDeps.page,
|
||||||
|
requestIP: (srv as any).requestIP,
|
||||||
|
}),
|
||||||
(req, url) => handleWebrtcRoutes(req, url),
|
(req, url) => handleWebrtcRoutes(req, url),
|
||||||
(req, url, srv) => handleClientRoutes(req, url, srv as any, routeDeps.client),
|
(req, url, srv) => handleClientRoutes(req, url, srv as any, routeDeps.client),
|
||||||
(req, url, srv) => handleWsUpgradeRoutes(req, url, srv as any, routeDeps.wsUpgrade),
|
(req, url, srv) => handleWsUpgradeRoutes(req, url, srv as any, routeDeps.wsUpgrade),
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
import { getConfig } from "./config";
|
import { getConfig } from "./config";
|
||||||
import { isRateLimited, recordFailedAttempt, recordSuccessfulAttempt } from "./rateLimit";
|
import {
|
||||||
|
clearRequestRateLimitsForTests,
|
||||||
|
consumeLoginPageRateLimit,
|
||||||
|
consumeUnauthorizedRateLimit,
|
||||||
|
isRateLimited,
|
||||||
|
recordFailedAttempt,
|
||||||
|
recordSuccessfulAttempt,
|
||||||
|
} from "./rateLimit";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clearRequestRateLimitsForTests();
|
||||||
|
});
|
||||||
|
|
||||||
describe("rateLimit", () => {
|
describe("rateLimit", () => {
|
||||||
test("locks out after repeated failures", () => {
|
test("locks out after repeated failures", () => {
|
||||||
@@ -26,4 +37,26 @@ describe("rateLimit", () => {
|
|||||||
recordSuccessfulAttempt(ip);
|
recordSuccessfulAttempt(ip);
|
||||||
expect(isRateLimited(ip).limited).toBe(false);
|
expect(isRateLimited(ip).limited).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("limits repeated login page requests by IP", () => {
|
||||||
|
const ip = `10.0.2.${Date.now()}`;
|
||||||
|
for (let i = 0; i < 60; i += 1) {
|
||||||
|
expect(consumeLoginPageRateLimit(ip).limited).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = consumeLoginPageRateLimit(ip);
|
||||||
|
expect(result.limited).toBe(true);
|
||||||
|
expect(result.retryAfter).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("limits repeated unauthorized responses by IP", () => {
|
||||||
|
const ip = `10.0.3.${Date.now()}`;
|
||||||
|
for (let i = 0; i < 120; i += 1) {
|
||||||
|
expect(consumeUnauthorizedRateLimit(ip).limited).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = consumeUnauthorizedRateLimit(ip);
|
||||||
|
expect(result.limited).toBe(true);
|
||||||
|
expect(result.retryAfter).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,10 +7,22 @@ interface RateLimitEntry {
|
|||||||
lockedUntil?: number;
|
lockedUntil?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequestRateLimitPolicy = {
|
||||||
|
maxRequests: number;
|
||||||
|
windowMs: number;
|
||||||
|
lockoutMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
const rateLimitStore = new Map<string, RateLimitEntry>();
|
const rateLimitStore = new Map<string, RateLimitEntry>();
|
||||||
|
const requestRateLimitStore = new Map<string, RateLimitEntry>();
|
||||||
|
|
||||||
const CLEANUP_INTERVAL = 60 * 1000;
|
const CLEANUP_INTERVAL = 60 * 1000;
|
||||||
|
|
||||||
|
function numberFromEnv(name: string, fallback: number): number {
|
||||||
|
const parsed = Number(process.env[name]);
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function getRateLimitPolicy() {
|
function getRateLimitPolicy() {
|
||||||
const security = getConfig().security;
|
const security = getConfig().security;
|
||||||
const maxAttempts = Math.min(50, Math.max(1, Number(security.loginMaxAttempts) || 5));
|
const maxAttempts = Math.min(50, Math.max(1, Number(security.loginMaxAttempts) || 5));
|
||||||
@@ -85,6 +97,75 @@ export function recordSuccessfulAttempt(ip: string): void {
|
|||||||
rateLimitStore.delete(ip);
|
rateLimitStore.delete(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeRequestPolicy(prefix: string, defaults: RequestRateLimitPolicy): RequestRateLimitPolicy {
|
||||||
|
return {
|
||||||
|
maxRequests: Math.min(
|
||||||
|
1000,
|
||||||
|
Math.max(1, numberFromEnv(`OVERLORD_${prefix}_RATE_LIMIT_MAX`, defaults.maxRequests)),
|
||||||
|
),
|
||||||
|
windowMs: Math.min(
|
||||||
|
60 * 60 * 1000,
|
||||||
|
Math.max(10 * 1000, numberFromEnv(`OVERLORD_${prefix}_RATE_LIMIT_WINDOW_SECONDS`, defaults.windowMs / 1000) * 1000),
|
||||||
|
),
|
||||||
|
lockoutMs: Math.min(
|
||||||
|
60 * 60 * 1000,
|
||||||
|
Math.max(10 * 1000, numberFromEnv(`OVERLORD_${prefix}_RATE_LIMIT_LOCKOUT_SECONDS`, defaults.lockoutMs / 1000) * 1000),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumeRequestRateLimit(
|
||||||
|
key: string,
|
||||||
|
label: string,
|
||||||
|
policy: RequestRateLimitPolicy,
|
||||||
|
): { limited: boolean; retryAfter?: number } {
|
||||||
|
const now = Date.now();
|
||||||
|
const entry = requestRateLimitStore.get(key);
|
||||||
|
|
||||||
|
if (entry?.lockedUntil && entry.lockedUntil > now) {
|
||||||
|
return { limited: true, retryAfter: Math.ceil((entry.lockedUntil - now) / 1000) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry || now - entry.firstAttempt > policy.windowMs) {
|
||||||
|
requestRateLimitStore.set(key, { attempts: 1, firstAttempt: now });
|
||||||
|
return { limited: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.attempts++;
|
||||||
|
if (entry.attempts > policy.maxRequests) {
|
||||||
|
entry.lockedUntil = now + policy.lockoutMs;
|
||||||
|
const retryAfter = Math.ceil(policy.lockoutMs / 1000);
|
||||||
|
logger.warn(
|
||||||
|
`[rate-limit] ${label} rate limit exceeded for ${key}; locked for ${retryAfter}s after ${entry.attempts} requests`,
|
||||||
|
);
|
||||||
|
return { limited: true, retryAfter };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { limited: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function consumeLoginPageRateLimit(ip: string): { limited: boolean; retryAfter?: number } {
|
||||||
|
const policy = makeRequestPolicy("LOGIN_PAGE", {
|
||||||
|
maxRequests: 60,
|
||||||
|
windowMs: 5 * 60 * 1000,
|
||||||
|
lockoutMs: 10 * 60 * 1000,
|
||||||
|
});
|
||||||
|
return consumeRequestRateLimit(`login-page:${ip}`, "Login page", policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function consumeUnauthorizedRateLimit(ip: string): { limited: boolean; retryAfter?: number } {
|
||||||
|
const policy = makeRequestPolicy("UNAUTHORIZED", {
|
||||||
|
maxRequests: 120,
|
||||||
|
windowMs: 5 * 60 * 1000,
|
||||||
|
lockoutMs: 10 * 60 * 1000,
|
||||||
|
});
|
||||||
|
return consumeRequestRateLimit(`unauthorized:${ip}`, "Unauthorized request", policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearRequestRateLimitsForTests(): void {
|
||||||
|
requestRateLimitStore.clear();
|
||||||
|
}
|
||||||
|
|
||||||
function cleanupExpired(): void {
|
function cleanupExpired(): void {
|
||||||
const policy = getRateLimitPolicy();
|
const policy = getRateLimitPolicy();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -100,6 +181,15 @@ function cleanupExpired(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [key, entry] of requestRateLimitStore.entries()) {
|
||||||
|
const expired = now - entry.firstAttempt > 60 * 60 * 1000;
|
||||||
|
const lockoutExpired = entry.lockedUntil && entry.lockedUntil < now;
|
||||||
|
if (expired || lockoutExpired) {
|
||||||
|
requestRateLimitStore.delete(key);
|
||||||
|
cleaned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cleaned > 0) {
|
if (cleaned > 0) {
|
||||||
logger.debug(`[rate-limit] Cleaned up ${cleaned} expired entries`);
|
logger.debug(`[rate-limit] Cleaned up ${cleaned} expired entries`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { clearRequestRateLimitsForTests } from "../rateLimit";
|
||||||
|
import { createHttpFetchHandler } from "./http-dispatch";
|
||||||
|
|
||||||
|
const metrics = {
|
||||||
|
withHttpMetrics: (fn: () => Promise<Response>) => fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeServer(ip: string) {
|
||||||
|
return {
|
||||||
|
requestIP: () => ({ address: ip }),
|
||||||
|
upgrade: () => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clearRequestRateLimitsForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createHttpFetchHandler rate limiting", () => {
|
||||||
|
test("throttles repeated unauthorized responses from the same IP", async () => {
|
||||||
|
const handler = createHttpFetchHandler({
|
||||||
|
metrics,
|
||||||
|
CORS_HEADERS: {},
|
||||||
|
routes: [async () => new Response("Unauthorized", { status: 401 })],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < 120; i += 1) {
|
||||||
|
const res = await handler(new Request("https://localhost/api/private"), makeServer("203.0.113.10"));
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const limited = await handler(new Request("https://localhost/api/private"), makeServer("203.0.113.10"));
|
||||||
|
expect(limited.status).toBe(429);
|
||||||
|
expect(limited.headers.get("Retry-After")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,18 @@
|
|||||||
import { wrapServerWithClientIp, type RequestServerLike } from "./client-ip";
|
import { wrapServerWithClientIp, type RequestServerLike } from "./client-ip";
|
||||||
|
import { consumeUnauthorizedRateLimit } from "../rateLimit";
|
||||||
|
|
||||||
export type RouteHandler = (req: Request, url: URL, server: unknown) => Promise<Response | null>;
|
export type RouteHandler = (req: Request, url: URL, server: unknown) => Promise<Response | null>;
|
||||||
|
|
||||||
|
function tooManyRequestsResponse(retryAfter = 60): Response {
|
||||||
|
return new Response("Too many requests", {
|
||||||
|
status: 429,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
|
"Retry-After": String(retryAfter),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function createHttpFetchHandler(deps: {
|
export function createHttpFetchHandler(deps: {
|
||||||
metrics: { withHttpMetrics: (fn: () => Promise<Response>) => Promise<Response> };
|
metrics: { withHttpMetrics: (fn: () => Promise<Response>) => Promise<Response> };
|
||||||
CORS_HEADERS: Record<string, string>;
|
CORS_HEADERS: Record<string, string>;
|
||||||
@@ -14,9 +25,17 @@ export function createHttpFetchHandler(deps: {
|
|||||||
}
|
}
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const wrapped = wrapServerWithClientIp(server as RequestServerLike);
|
const wrapped = wrapServerWithClientIp(server as RequestServerLike);
|
||||||
|
const ip = wrapped.requestIP(req)?.address || "unknown";
|
||||||
|
|
||||||
for (const route of deps.routes) {
|
for (const route of deps.routes) {
|
||||||
const response = await route(req, url, wrapped);
|
const response = await route(req, url, wrapped);
|
||||||
if (response) return response;
|
if (response) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
const limited = consumeUnauthorizedRateLimit(ip);
|
||||||
|
if (limited.limited) return tooManyRequestsResponse(limited.retryAfter);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new Response("Not found", { status: 404 });
|
return new Response("Not found", { status: 404 });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { authenticateRequest } from "../../auth";
|
import { authenticateRequest } from "../../auth";
|
||||||
import { getConfig } from "../../config";
|
import { getConfig } from "../../config";
|
||||||
|
import { consumeLoginPageRateLimit } from "../../rateLimit";
|
||||||
import { requirePermission, type Permission } from "../../rbac";
|
import { requirePermission, type Permission } from "../../rbac";
|
||||||
import { canUserAccessClient, getUserById, type UserRole } from "../../users";
|
import { canUserAccessClient, getUserById, type UserRole } from "../../users";
|
||||||
|
|
||||||
@@ -7,9 +8,24 @@ type PageRouteDeps = {
|
|||||||
PUBLIC_ROOT: string;
|
PUBLIC_ROOT: string;
|
||||||
secureHeaders: (contentType?: string) => Record<string, string>;
|
secureHeaders: (contentType?: string) => Record<string, string>;
|
||||||
mimeType: (path: string) => string;
|
mimeType: (path: string) => string;
|
||||||
|
requestIP?: (req: Request) => { address?: string } | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function serveLoginOrUnauthorized(deps: PageRouteDeps): Promise<Response> {
|
function tooManyRequestsResponse(retryAfter = 60): Response {
|
||||||
|
return new Response("Too many requests", {
|
||||||
|
status: 429,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
|
"Retry-After": String(retryAfter),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serveLoginOrUnauthorized(req: Request, deps: PageRouteDeps): Promise<Response> {
|
||||||
|
const ip = deps.requestIP?.(req)?.address || "unknown";
|
||||||
|
const limited = consumeLoginPageRateLimit(ip);
|
||||||
|
if (limited.limited) return tooManyRequestsResponse(limited.retryAfter);
|
||||||
|
|
||||||
const loginFile = Bun.file(`${deps.PUBLIC_ROOT}/login.html`);
|
const loginFile = Bun.file(`${deps.PUBLIC_ROOT}/login.html`);
|
||||||
if (await loginFile.exists()) {
|
if (await loginFile.exists()) {
|
||||||
return new Response(loginFile, { headers: deps.secureHeaders(deps.mimeType("/login.html")) });
|
return new Response(loginFile, { headers: deps.secureHeaders(deps.mimeType("/login.html")) });
|
||||||
@@ -143,10 +159,19 @@ export async function handlePageRoutes(
|
|||||||
const filePath = authed ? "/index.html" : "/login.html";
|
const filePath = authed ? "/index.html" : "/login.html";
|
||||||
const file = Bun.file(`${deps.PUBLIC_ROOT}${filePath}`);
|
const file = Bun.file(`${deps.PUBLIC_ROOT}${filePath}`);
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
|
if (!authed) {
|
||||||
|
const limited = consumeLoginPageRateLimit(deps.requestIP?.(req)?.address || "unknown");
|
||||||
|
if (limited.limited) return tooManyRequestsResponse(limited.retryAfter);
|
||||||
|
}
|
||||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType(filePath)) });
|
return new Response(file, { headers: deps.secureHeaders(deps.mimeType(filePath)) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Explicit login page ----
|
||||||
|
if (url.pathname === "/login.html") {
|
||||||
|
return serveLoginOrUnauthorized(req, deps);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Change password (always accessible) ----
|
// ---- Change password (always accessible) ----
|
||||||
if (url.pathname === "/change-password.html") {
|
if (url.pathname === "/change-password.html") {
|
||||||
return serveFile(deps, "change-password.html");
|
return serveFile(deps, "change-password.html");
|
||||||
@@ -162,7 +187,7 @@ export async function handlePageRoutes(
|
|||||||
if (url.pathname !== page.path) continue;
|
if (url.pathname !== page.path) continue;
|
||||||
|
|
||||||
const user = await authenticateRequest(req);
|
const user = await authenticateRequest(req);
|
||||||
if (!user) return serveLoginOrUnauthorized(deps);
|
if (!user) return serveLoginOrUnauthorized(req, deps);
|
||||||
|
|
||||||
if (page.path === "/screenshots" && !getConfig().thumbnails.wallEnabled) {
|
if (page.path === "/screenshots" && !getConfig().thumbnails.wallEnabled) {
|
||||||
return new Response("Screenshot wall is disabled by the administrator", { status: 403 });
|
return new Response("Screenshot wall is disabled by the administrator", { status: 403 });
|
||||||
@@ -193,7 +218,7 @@ export async function handlePageRoutes(
|
|||||||
if (url.pathname !== page.path) continue;
|
if (url.pathname !== page.path) continue;
|
||||||
|
|
||||||
const user = await authenticateRequest(req);
|
const user = await authenticateRequest(req);
|
||||||
if (!user) return serveLoginOrUnauthorized(deps);
|
if (!user) return serveLoginOrUnauthorized(req, deps);
|
||||||
|
|
||||||
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
|
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
|
||||||
if (maybeChange) return maybeChange;
|
if (maybeChange) return maybeChange;
|
||||||
@@ -216,7 +241,7 @@ export async function handlePageRoutes(
|
|||||||
if (!match) continue;
|
if (!match) continue;
|
||||||
|
|
||||||
const user = await authenticateRequest(req);
|
const user = await authenticateRequest(req);
|
||||||
if (!user) return serveLoginOrUnauthorized(deps);
|
if (!user) return serveLoginOrUnauthorized(req, deps);
|
||||||
|
|
||||||
if (user.role === "viewer") {
|
if (user.role === "viewer") {
|
||||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||||
|
|||||||
@@ -27,12 +27,10 @@ if errorlevel 1 (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo [1/10] Building server bundle...
|
echo [1/10] Building server bundle and web assets...
|
||||||
pushd "%SERVER_SRC%"
|
pushd "%SERVER_SRC%"
|
||||||
call bun install
|
call bun install
|
||||||
if errorlevel 1 goto :err
|
if errorlevel 1 goto :err
|
||||||
call bun run build:css
|
|
||||||
if errorlevel 1 goto :err
|
|
||||||
call bun run build
|
call bun run build
|
||||||
if errorlevel 1 goto :err
|
if errorlevel 1 goto :err
|
||||||
echo [2/10] Compiling Windows production executable...
|
echo [2/10] Compiling Windows production executable...
|
||||||
|
|||||||
Reference in New Issue
Block a user