mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
fix(auth): require admin auth for backup and translator routes
Protect database backup, export, restore, and translator save endpoints with authentication checks to block unauthenticated data access and state changes. Also remove the insecure API key secret fallback, ignore nested app env files from package publishes, and align tests with explicit application/json Accept headers for non-stream requests
This commit is contained in:
@@ -28,6 +28,8 @@ scripts/
|
||||
.vscode/
|
||||
.agents/
|
||||
.env*
|
||||
app/.env
|
||||
app/.env*
|
||||
eslint.config.mjs
|
||||
prettier.config.mjs
|
||||
postcss.config.mjs
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getDbInstance, SQLITE_FILE } from "@/lib/db/core";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
|
||||
/**
|
||||
* GET /api/db-backups/exportAll
|
||||
* Exports the entire database + settings as a ZIP archive
|
||||
* Security: Requires admin authentication.
|
||||
*/
|
||||
export async function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!SQLITE_FILE) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { listDbBackups, restoreDbBackup, backupDbFile } from "@/lib/localDb";
|
||||
import { dbBackupRestoreSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
|
||||
/**
|
||||
* PUT /api/db-backups — Trigger a manual backup snapshot.
|
||||
* Security: Requires admin authentication.
|
||||
*/
|
||||
export async function PUT() {
|
||||
export async function PUT(request: NextRequest) {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = backupDbFile("manual");
|
||||
if (!result) {
|
||||
@@ -21,8 +27,13 @@ export async function PUT() {
|
||||
|
||||
/**
|
||||
* GET /api/db-backups — List available database backups.
|
||||
* Security: Requires admin authentication.
|
||||
*/
|
||||
export async function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const backups = await listDbBackups();
|
||||
return NextResponse.json({ backups });
|
||||
@@ -35,8 +46,13 @@ export async function GET() {
|
||||
/**
|
||||
* POST /api/db-backups — Restore a specific backup.
|
||||
* Body: { backupId: "db_2026-02-11T14-00-00-000Z_pre-write.json" }
|
||||
* Security: Requires admin authentication.
|
||||
*/
|
||||
export async function POST(request) {
|
||||
export async function POST(request: NextRequest) {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { translatorSaveSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
export async function POST(request) {
|
||||
let rawBody;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
|
||||
@@ -6,9 +6,12 @@ if (!process.env.API_KEY_SECRET) {
|
||||
}
|
||||
|
||||
function getApiKeySecret(): string {
|
||||
const secret = process.env.API_KEY_SECRET || "omniroute-default-insecure-api-key-secret";
|
||||
const secret = process.env.API_KEY_SECRET;
|
||||
if (!secret || secret.trim() === "") {
|
||||
throw new Error("API_KEY_SECRET is required for API key CRC operations");
|
||||
throw new Error(
|
||||
"API_KEY_SECRET is required for API key CRC operations. " +
|
||||
"The startup validator (instrumentation-node.ts) should have set this automatically."
|
||||
);
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
@@ -365,9 +365,9 @@ test("chatCore resolves stream mode from body.stream and Accept header", async (
|
||||
});
|
||||
|
||||
assert.equal(explicitTrue.call.headers.Accept, "text/event-stream");
|
||||
assert.equal(explicitFalse.call.headers.Accept, undefined);
|
||||
assert.equal(explicitFalse.call.headers.Accept, "application/json");
|
||||
assert.equal(acceptDriven.call.headers.Accept, "text/event-stream");
|
||||
assert.equal(jsonDefault.call.headers.Accept, undefined);
|
||||
assert.equal(jsonDefault.call.headers.Accept, "application/json");
|
||||
});
|
||||
|
||||
test("chatCore injects memories when enabled and memories are found", async () => {
|
||||
|
||||
@@ -67,7 +67,7 @@ test("CodexExecutor.buildHeaders binds workspace ids and disables SSE accept for
|
||||
assert.equal(standardHeaders.Authorization, "Bearer codex-token");
|
||||
assert.equal(standardHeaders.Accept, "text/event-stream");
|
||||
assert.equal(standardHeaders["chatgpt-account-id"], "workspace-1");
|
||||
assert.equal(compactHeaders.Accept, undefined);
|
||||
assert.equal(compactHeaders.Accept, "application/json");
|
||||
});
|
||||
|
||||
test("CodexExecutor.transformRequest injects default instructions, clamps reasoning and strips unsupported fields", () => {
|
||||
|
||||
@@ -249,7 +249,7 @@ test("CodexExecutor does not request SSE accept header for compact requests", ()
|
||||
},
|
||||
false
|
||||
);
|
||||
assert.equal(headers.Accept, undefined);
|
||||
assert.equal(headers.Accept, "application/json");
|
||||
});
|
||||
|
||||
test("CodexExecutor preserves native responses payloads for Codex passthrough", () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ test("QoderExecutor: buildHeaders inherits configured user agent, auth and strea
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Qoder-Cli",
|
||||
Authorization: "Bearer token",
|
||||
Accept: "application/json",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,7 +116,10 @@ test("validateQoderCliPat succeeds when the validation endpoint returns OK", asy
|
||||
if (urlStr.includes("/ping")) {
|
||||
return new Response("pong", { status: 200 });
|
||||
}
|
||||
assert.match(urlStr, /api1\.qoder\.sh\/algo\/api\/v2\/service\/pro\/sse\/agent_chat_generation/);
|
||||
assert.match(
|
||||
urlStr,
|
||||
/api1\.qoder\.sh\/algo\/api\/v2\/service\/pro\/sse\/agent_chat_generation/
|
||||
);
|
||||
assert.equal(options.method, "POST");
|
||||
assert.match(String(options.headers.Authorization), /^Bearer COSY\./);
|
||||
return new Response("{}", { status: 200 });
|
||||
|
||||
Reference in New Issue
Block a user