mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* feat(i18n): add Turkish locale-aware text helpers (search/sort) * test(i18n): cover null/whitespace edges + document compareTr/normalize contract * fix(i18n): route dashboard search through Turkish-safe matchesSearch * fix(i18n): sort user-visible lists with Turkish collation (compareTr) * fix(i18n): keep providerId tiebreaker as ASCII sort (technical id) * docs(i18n): document intentional lang=en in global-error boundary * chore(lint): guard against locale-unsafe toLowerCase().includes search * fix(i18n): migrate missed provider-name search + harden lint disable placement * fix(i18n): downgrade no-restricted-syntax to warn (incremental adoption) The rule errored on ~19 pre-existing toLowerCase().includes() call-sites in src/app accumulated since this PR's base. Keep it as a warning so the guard-rail guides future code without breaking the 0-errors lint gate (project policy: 0 errors, warnings tolerated). --------- Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
309 lines
8.7 KiB
TypeScript
309 lines
8.7 KiB
TypeScript
"use server";
|
|
|
|
import { NextResponse } from "next/server";
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
|
import { ensureCliConfigWriteAllowed, getCliConfigPaths } from "@/shared/services/cliRuntime";
|
|
import { resolveDataDir } from "@/lib/dataPaths";
|
|
import { compareTr } from "@/shared/utils/turkishText";
|
|
import { codexProfileIdSchema, codexProfileNameSchema } from "@/shared/validation/schemas";
|
|
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
|
|
|
const PROFILES_DIR = path.join(resolveDataDir(), "codex-profiles");
|
|
|
|
/**
|
|
* Resolve a path inside PROFILES_DIR and verify it stays within bounds.
|
|
* Throws on path traversal attempts.
|
|
*/
|
|
function safeProfilePath(...segments: string[]): string {
|
|
const resolved = path.resolve(PROFILES_DIR, ...segments);
|
|
const base = path.resolve(PROFILES_DIR);
|
|
if (resolved !== base && !resolved.startsWith(base + path.sep)) {
|
|
throw new Error("Invalid path: directory traversal detected");
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
/**
|
|
* Ensure profiles directory exists
|
|
*/
|
|
async function ensureProfilesDir() {
|
|
await fs.mkdir(PROFILES_DIR, { recursive: true });
|
|
return PROFILES_DIR;
|
|
}
|
|
|
|
/**
|
|
* Extract a label from auth.json content (email or auth_mode)
|
|
*/
|
|
function extractAuthLabel(authJson) {
|
|
try {
|
|
const data = JSON.parse(authJson);
|
|
// ChatGPT-style auth
|
|
if (data.tokens?.id_token) {
|
|
const payload = data.tokens.id_token.split(".")[1];
|
|
const decoded = JSON.parse(Buffer.from(payload, "base64").toString());
|
|
if (decoded.email) return decoded.email;
|
|
}
|
|
if (data.auth_mode) return data.auth_mode;
|
|
if (data.OPENAI_API_KEY) return `API Key: ${data.OPENAI_API_KEY.slice(0, 8)}...`;
|
|
return "unknown";
|
|
} catch {
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
// GET - List all saved profiles
|
|
export async function GET(request: Request) {
|
|
const authError = await requireCliToolsAuth(request);
|
|
if (authError) return authError;
|
|
|
|
try {
|
|
await ensureProfilesDir();
|
|
|
|
let entries;
|
|
try {
|
|
entries = await fs.readdir(PROFILES_DIR);
|
|
} catch {
|
|
return NextResponse.json({ profiles: [] });
|
|
}
|
|
|
|
const profileFiles = entries.filter((e) => e.endsWith(".json"));
|
|
const profiles = [];
|
|
|
|
for (const file of profileFiles) {
|
|
try {
|
|
const raw = await fs.readFile(path.join(PROFILES_DIR, file), "utf-8");
|
|
const profile = JSON.parse(raw);
|
|
profiles.push({
|
|
id: file.replace(".json", ""),
|
|
name: profile.name,
|
|
authLabel: profile.authLabel || "unknown",
|
|
createdAt: profile.createdAt,
|
|
hasConfig: !!profile.configToml,
|
|
hasAuth: !!profile.authJson,
|
|
});
|
|
} catch {
|
|
// Skip corrupt files
|
|
}
|
|
}
|
|
|
|
// Sort by name
|
|
profiles.sort((a, b) => compareTr(a.name, b.name));
|
|
return NextResponse.json({ profiles });
|
|
} catch (error) {
|
|
console.log("Error listing codex profiles:", error.message);
|
|
return NextResponse.json({ error: "Failed to list profiles" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// POST - Save current config as a named profile
|
|
export async function POST(request) {
|
|
const authError = await requireCliToolsAuth(request);
|
|
if (authError) return authError;
|
|
|
|
let rawBody;
|
|
try {
|
|
rawBody = await request.json();
|
|
} catch {
|
|
return NextResponse.json(
|
|
{
|
|
error: {
|
|
message: "Invalid request",
|
|
details: [{ field: "body", message: "Invalid JSON body" }],
|
|
},
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
try {
|
|
const writeGuard = ensureCliConfigWriteAllowed();
|
|
if (writeGuard) {
|
|
return NextResponse.json({ error: writeGuard }, { status: 403 });
|
|
}
|
|
|
|
const validation = validateBody(codexProfileNameSchema, rawBody);
|
|
if (isValidationFailure(validation)) {
|
|
return NextResponse.json({ error: validation.error }, { status: 400 });
|
|
}
|
|
const { name } = validation.data;
|
|
|
|
const paths = getCliConfigPaths("codex");
|
|
if (!paths) {
|
|
return NextResponse.json({ error: "Codex config paths not found" }, { status: 500 });
|
|
}
|
|
|
|
// Read current files
|
|
let configToml = null;
|
|
let authJson = null;
|
|
|
|
try {
|
|
configToml = await fs.readFile(paths.config, "utf-8");
|
|
} catch {
|
|
// No config file
|
|
}
|
|
|
|
try {
|
|
authJson = await fs.readFile(paths.auth, "utf-8");
|
|
} catch {
|
|
// No auth file
|
|
}
|
|
|
|
if (!configToml && !authJson) {
|
|
return NextResponse.json(
|
|
{ error: "No Codex configuration files found to save" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const profileId = name
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-|-$/g, "");
|
|
|
|
const profile = {
|
|
name: name.trim(),
|
|
createdAt: new Date().toISOString(),
|
|
authLabel: authJson ? extractAuthLabel(authJson) : "no-auth",
|
|
configToml,
|
|
authJson,
|
|
};
|
|
|
|
await ensureProfilesDir();
|
|
const profilePath = safeProfilePath(`${profileId}.json`);
|
|
await fs.writeFile(profilePath, JSON.stringify(profile, null, 2));
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: `Profile "${name}" saved successfully`,
|
|
profileId,
|
|
});
|
|
} catch (error) {
|
|
console.log("Error saving codex profile:", error.message);
|
|
return NextResponse.json({ error: "Failed to save profile" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// PUT - Activate a saved profile (restore its config + auth)
|
|
export async function PUT(request) {
|
|
const authError = await requireCliToolsAuth(request);
|
|
if (authError) return authError;
|
|
|
|
let rawBody;
|
|
try {
|
|
rawBody = await request.json();
|
|
} catch {
|
|
return NextResponse.json(
|
|
{
|
|
error: {
|
|
message: "Invalid request",
|
|
details: [{ field: "body", message: "Invalid JSON body" }],
|
|
},
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
try {
|
|
const writeGuard = ensureCliConfigWriteAllowed();
|
|
if (writeGuard) {
|
|
return NextResponse.json({ error: writeGuard }, { status: 403 });
|
|
}
|
|
|
|
const validation = validateBody(codexProfileIdSchema, rawBody);
|
|
if (isValidationFailure(validation)) {
|
|
return NextResponse.json({ error: validation.error }, { status: 400 });
|
|
}
|
|
const { profileId } = validation.data;
|
|
|
|
const profilePath = safeProfilePath(`${profileId}.json`);
|
|
let profile;
|
|
try {
|
|
const raw = await fs.readFile(profilePath, "utf-8");
|
|
profile = JSON.parse(raw);
|
|
} catch {
|
|
return NextResponse.json({ error: `Profile "${profileId}" not found` }, { status: 404 });
|
|
}
|
|
|
|
const paths = getCliConfigPaths("codex");
|
|
if (!paths) {
|
|
return NextResponse.json({ error: "Codex config paths not found" }, { status: 500 });
|
|
}
|
|
|
|
// Create backup of current config before switching
|
|
const { createMultiBackup } = await import("@/shared/services/backupService");
|
|
await createMultiBackup("codex", [paths.config, paths.auth]);
|
|
|
|
// Ensure codex dir exists
|
|
await fs.mkdir(path.dirname(paths.config), { recursive: true });
|
|
|
|
// Restore files
|
|
if (profile.configToml) {
|
|
await fs.writeFile(paths.config, profile.configToml);
|
|
}
|
|
if (profile.authJson) {
|
|
await fs.writeFile(paths.auth, profile.authJson);
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: `Profile "${profile.name}" activated`,
|
|
profileId,
|
|
restoredConfig: !!profile.configToml,
|
|
restoredAuth: !!profile.authJson,
|
|
});
|
|
} catch (error) {
|
|
console.log("Error activating codex profile:", error.message);
|
|
return NextResponse.json({ error: "Failed to activate profile" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// DELETE - Remove a saved profile
|
|
export async function DELETE(request) {
|
|
const authError = await requireCliToolsAuth(request);
|
|
if (authError) return authError;
|
|
|
|
let rawBody;
|
|
try {
|
|
rawBody = await request.json();
|
|
} catch {
|
|
return NextResponse.json(
|
|
{
|
|
error: {
|
|
message: "Invalid request",
|
|
details: [{ field: "body", message: "Invalid JSON body" }],
|
|
},
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
try {
|
|
const validation = validateBody(codexProfileIdSchema, rawBody);
|
|
if (isValidationFailure(validation)) {
|
|
return NextResponse.json({ error: validation.error }, { status: 400 });
|
|
}
|
|
const { profileId } = validation.data;
|
|
|
|
const profilePath = safeProfilePath(`${profileId}.json`);
|
|
try {
|
|
await fs.unlink(profilePath);
|
|
} catch (err) {
|
|
if (err.code === "ENOENT") {
|
|
return NextResponse.json({ error: `Profile "${profileId}" not found` }, { status: 404 });
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: `Profile "${profileId}" deleted`,
|
|
});
|
|
} catch (error) {
|
|
console.log("Error deleting codex profile:", error.message);
|
|
return NextResponse.json({ error: "Failed to delete profile" }, { status: 500 });
|
|
}
|
|
}
|