mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
fix(ui): correct Quick Start API key link to /dashboard/endpoint
- HomePageClient.tsx: 'Settings → API Keys' → 'Endpoint → Registered Keys' - docs/page.tsx: same fix in Quick Start step 2 - Also includes user's manual fixes: bodySizeGuard, container imports, migrationRunner ESM compat
This commit is contained in:
@@ -158,10 +158,10 @@ export default function HomePageClient({ machineId }) {
|
||||
<span className="font-semibold">1. Create API key</span>
|
||||
<p className="text-text-muted mt-0.5">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/settings" className="text-primary hover:underline">
|
||||
Settings
|
||||
<Link href="/dashboard/endpoint" className="text-primary hover:underline">
|
||||
Endpoint
|
||||
</Link>{" "}
|
||||
→ API Keys. Generate one key per environment.
|
||||
→ Registered Keys. Generate one key per environment.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -23,26 +23,51 @@ export default function SessionInfoCard() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Build session info from client-side data
|
||||
const loginTime = sessionStorage.getItem("omniroute_login_time");
|
||||
const now = Date.now();
|
||||
let cancelled = false;
|
||||
|
||||
let sessionAge = "Unknown";
|
||||
if (loginTime) {
|
||||
const elapsed = now - parseInt(loginTime, 10);
|
||||
const hours = Math.floor(elapsed / 3600000);
|
||||
const minutes = Math.floor((elapsed % 3600000) / 60000);
|
||||
sessionAge = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
||||
async function loadSession() {
|
||||
// Build session info from client-side data
|
||||
const loginTime = sessionStorage.getItem("omniroute_login_time");
|
||||
const now = Date.now();
|
||||
|
||||
let sessionAge = "Unknown";
|
||||
if (loginTime) {
|
||||
const elapsed = now - parseInt(loginTime, 10);
|
||||
const hours = Math.floor(elapsed / 3600000);
|
||||
const minutes = Math.floor((elapsed % 3600000) / 60000);
|
||||
sessionAge = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
||||
}
|
||||
|
||||
let authenticated = false;
|
||||
try {
|
||||
const res = await fetch("/api/auth/status", {
|
||||
method: "GET",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
authenticated = data.authenticated === true;
|
||||
}
|
||||
} catch {
|
||||
// Keep unauthenticated fallback on network errors.
|
||||
}
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
setSession({
|
||||
authenticated,
|
||||
loginTime: loginTime ? new Date(parseInt(loginTime, 10)).toLocaleString() : null,
|
||||
sessionAge,
|
||||
ipAddress: "—", // Server-side only
|
||||
userAgent: navigator.userAgent.split(" ").slice(-2).join(" ") || "Unknown",
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
setSession({
|
||||
authenticated: !!document.cookie.includes("omniroute"),
|
||||
loginTime: loginTime ? new Date(parseInt(loginTime, 10)).toLocaleString() : null,
|
||||
sessionAge,
|
||||
ipAddress: "—", // Server-side only
|
||||
userAgent: navigator.userAgent.split(" ").slice(-2).join(" ") || "Unknown",
|
||||
});
|
||||
setLoading(false);
|
||||
loadSession();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleLogout = async () => {
|
||||
|
||||
23
src/app/api/auth/status/route.ts
Normal file
23
src/app/api/auth/status/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
import { jwtVerify } from "jose";
|
||||
|
||||
const SECRET = process.env.JWT_SECRET
|
||||
? new TextEncoder().encode(process.env.JWT_SECRET)
|
||||
: null;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get("auth_token")?.value;
|
||||
|
||||
if (!token || !SECRET) {
|
||||
return NextResponse.json({ authenticated: false });
|
||||
}
|
||||
|
||||
await jwtVerify(token, SECRET);
|
||||
return NextResponse.json({ authenticated: true });
|
||||
} catch {
|
||||
return NextResponse.json({ authenticated: false });
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ export default function DocsPage() {
|
||||
<li className="rounded-lg border border-border p-3 bg-bg">
|
||||
<span className="font-semibold">2. Create API key</span>
|
||||
<p className="text-text-muted mt-1">
|
||||
Go to Settings → API Keys. Generate one key per environment.
|
||||
Go to Endpoint → Registered Keys. Generate one key per environment.
|
||||
</p>
|
||||
</li>
|
||||
<li className="rounded-lg border border-border p-3 bg-bg">
|
||||
|
||||
@@ -56,6 +56,7 @@ export default function LoginPage() {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
sessionStorage.setItem("omniroute_login_time", String(Date.now()));
|
||||
router.push("/dashboard");
|
||||
router.refresh();
|
||||
} else {
|
||||
|
||||
@@ -15,6 +15,18 @@
|
||||
* @module lib/container
|
||||
*/
|
||||
|
||||
import { evaluateFirstAllowed, evaluateRequest, PolicyEngine } from "../domain/policyEngine";
|
||||
import { getDbInstance } from "./db/core";
|
||||
import {
|
||||
decrypt,
|
||||
decryptConnectionFields,
|
||||
encrypt,
|
||||
encryptConnectionFields,
|
||||
} from "./db/encryption";
|
||||
import { getSettings } from "./localDb";
|
||||
import { getCircuitBreaker } from "../shared/utils/circuitBreaker";
|
||||
import { recordTelemetry, RequestTelemetry } from "../shared/utils/requestTelemetry";
|
||||
|
||||
type Factory<T = any> = () => T;
|
||||
|
||||
class Container {
|
||||
@@ -76,41 +88,34 @@ class Container {
|
||||
export const container = new Container();
|
||||
|
||||
// ── Default registrations ──
|
||||
// These lazy-load the actual modules so the container can be imported
|
||||
// without triggering all side effects at module load time.
|
||||
// Services are still lazily instantiated on first resolve().
|
||||
|
||||
container.register("settings", () => {
|
||||
const { getSettings } = require("@/lib/localDb");
|
||||
return { get: getSettings };
|
||||
});
|
||||
|
||||
container.register("db", () => {
|
||||
const { getDbInstance } = require("@/lib/db/core");
|
||||
return getDbInstance();
|
||||
});
|
||||
|
||||
container.register("encryption", () => {
|
||||
const enc = require("@/lib/db/encryption");
|
||||
return {
|
||||
encrypt: enc.encrypt,
|
||||
decrypt: enc.decrypt,
|
||||
encryptConnectionFields: enc.encryptConnectionFields,
|
||||
decryptConnectionFields: enc.decryptConnectionFields,
|
||||
encrypt,
|
||||
decrypt,
|
||||
encryptConnectionFields,
|
||||
decryptConnectionFields,
|
||||
};
|
||||
});
|
||||
|
||||
container.register("policyEngine", () => {
|
||||
const { evaluateRequest, evaluateFirstAllowed, PolicyEngine } = require("@/domain/policyEngine");
|
||||
return { evaluateRequest, evaluateFirstAllowed, PolicyEngine };
|
||||
});
|
||||
|
||||
container.register("circuitBreaker", () => {
|
||||
const { getCircuitBreaker } = require("@/shared/utils/circuitBreaker");
|
||||
return { get: getCircuitBreaker };
|
||||
});
|
||||
|
||||
container.register("telemetry", () => {
|
||||
const { RequestTelemetry, recordTelemetry } = require("@/shared/utils/requestTelemetry");
|
||||
return { RequestTelemetry, recordTelemetry };
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import Database from "better-sqlite3";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { resolveDataDir, getLegacyDotDataDir } from "../dataPaths";
|
||||
import { runMigrations } from "./migrationRunner";
|
||||
|
||||
// ──────────────── Environment Detection ────────────────
|
||||
|
||||
@@ -355,21 +356,16 @@ export function getDbInstance() {
|
||||
// ── Versioned Migrations ──
|
||||
// Auto-seed 001 as applied (the inline SCHEMA_SQL already created these tables)
|
||||
// then run any new migrations (002+)
|
||||
try {
|
||||
const { runMigrations } = require("./migrationRunner");
|
||||
_db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS _omniroute_migrations (
|
||||
version TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
INSERT OR IGNORE INTO _omniroute_migrations (version, name)
|
||||
VALUES ('001', 'initial_schema');
|
||||
`);
|
||||
runMigrations(_db);
|
||||
} catch (err: any) {
|
||||
console.warn("[DB] Migration runner unavailable:", err.message);
|
||||
}
|
||||
_db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS _omniroute_migrations (
|
||||
version TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
INSERT OR IGNORE INTO _omniroute_migrations (version, name)
|
||||
VALUES ('001', 'initial_schema');
|
||||
`);
|
||||
runMigrations(_db);
|
||||
|
||||
// Auto-migrate from db.json if exists
|
||||
if (JSON_DB_FILE && fs.existsSync(JSON_DB_FILE)) {
|
||||
|
||||
@@ -11,8 +11,11 @@
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type Database from "better-sqlite3";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const MIGRATIONS_DIR = path.join(__dirname, "migrations");
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @module lib/evals/scheduler
|
||||
*/
|
||||
|
||||
import { runSuite, listSuites, createScorecard } from "./evalRunner";
|
||||
import { runSuite, listSuites, createScorecard, getSuite } from "./evalRunner";
|
||||
|
||||
// ── Types ──
|
||||
|
||||
@@ -159,7 +159,6 @@ async function executeScheduledRun(suiteId: string): Promise<EvalRunResult | nul
|
||||
// Get outputs from provider
|
||||
const outputs: Record<string, string> = {};
|
||||
// We use the suite's cases to get the case IDs
|
||||
const { getSuite } = require("./evalRunner");
|
||||
const suite = getSuite(suiteId);
|
||||
if (!suite?.cases) return null;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { jwtVerify } from "jose";
|
||||
import { generateRequestId } from "./shared/utils/requestId";
|
||||
import { getSettings } from "./lib/localDb";
|
||||
import { isPublicRoute, verifyAuth, isAuthRequired } from "./shared/utils/apiAuth";
|
||||
import { checkBodySize } from "./shared/middleware/bodySizeGuard";
|
||||
import { checkBodySize, getBodySizeLimit } from "./shared/middleware/bodySizeGuard";
|
||||
import { isDraining } from "./lib/gracefulShutdown";
|
||||
|
||||
// FASE-01: Fail-fast — no hardcoded fallback. Server must have JWT_SECRET configured.
|
||||
@@ -37,7 +37,7 @@ export async function proxy(request) {
|
||||
|
||||
// ──────────────── Pre-flight: Reject oversized bodies ────────────────
|
||||
if (pathname.startsWith("/api/") && request.method !== "GET" && request.method !== "OPTIONS") {
|
||||
const bodySizeRejection = checkBodySize(request);
|
||||
const bodySizeRejection = checkBodySize(request, getBodySizeLimit(pathname));
|
||||
if (bodySizeRejection) return bodySizeRejection;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,30 @@ const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024;
|
||||
/** Larger limit for backup/import routes: 100 MB */
|
||||
export const MAX_BODY_BYTES_IMPORT = 100 * 1024 * 1024;
|
||||
|
||||
/** Larger limit for audio transcription uploads: 100 MB */
|
||||
export const MAX_BODY_BYTES_AUDIO = 100 * 1024 * 1024;
|
||||
|
||||
/** Configured limit — reads from env or falls back to 10 MB */
|
||||
export const MAX_BODY_BYTES = parseInt(
|
||||
process.env.MAX_BODY_SIZE_BYTES || String(DEFAULT_MAX_BODY_BYTES),
|
||||
10
|
||||
);
|
||||
|
||||
type BodySizeRule = { prefix: string; limit: number };
|
||||
|
||||
const ROUTE_LIMITS: BodySizeRule[] = [
|
||||
{ prefix: "/api/db-backups/import", limit: MAX_BODY_BYTES_IMPORT },
|
||||
{ prefix: "/api/v1/audio/transcriptions", limit: MAX_BODY_BYTES_AUDIO },
|
||||
];
|
||||
|
||||
/**
|
||||
* Resolve the body size limit for a request path.
|
||||
*/
|
||||
export function getBodySizeLimit(pathname: string): number {
|
||||
const customRule = ROUTE_LIMITS.find((rule) => pathname.startsWith(rule.prefix));
|
||||
return customRule?.limit ?? MAX_BODY_BYTES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Content-Length header against the configured limit.
|
||||
* Returns a 413 Response if the body is too large, or null if OK.
|
||||
|
||||
@@ -20,6 +20,7 @@ const PUBLIC_API_ROUTES = [
|
||||
// Auth flow — must be accessible to unauthenticated users
|
||||
"/api/auth/login",
|
||||
"/api/auth/logout",
|
||||
"/api/auth/status",
|
||||
|
||||
// Settings check — used by login page / onboarding
|
||||
"/api/settings/require-login",
|
||||
|
||||
Reference in New Issue
Block a user