feat: add admin panel, Replicate AI translation, and document translation

- Admin panel (/admin) with JWT auth: configure Replicate API token,
  JigsawStack API key, model version, enable/disable AI translation,
  change admin password. Settings persisted in data/settings.json.

- Replicate AI translation: POST /api/translate/replicate uses
  JigsawStack text-translate model via Replicate API. Main page
  switches to client-side AI translation when enabled.

- Document translation tab: supports PDF, DOCX, XLSX, XLS, CSV.
  Excel/Word formatting fully preserved (SheetJS + JSZip XML manipulation).
  PDF uses pdf-parse extraction + pdf-lib reconstruction.
  Column selector UI for tabular data (per-sheet, All/None toggles).

- Updated README with full implementation documentation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 07:43:54 +01:00
parent 0190ea5da9
commit 0799101da3
23 changed files with 18595 additions and 261 deletions

33
pages/api/admin/auth.ts Normal file
View File

@@ -0,0 +1,33 @@
import { NextApiHandler } from "next";
import { signAdminToken, checkPassword, COOKIE_NAME, getTokenFromRequest, verifyAdminToken } from "@utils/admin-auth";
const handler: NextApiHandler = async (req, res) => {
if (req.method === "POST") {
const { password } = req.body ?? {};
if (!password || typeof password !== "string") {
return res.status(400).json({ error: "Password required" });
}
if (!checkPassword(password)) {
return res.status(401).json({ error: "Invalid password" });
}
const token = await signAdminToken();
res.setHeader("Set-Cookie", `${COOKIE_NAME}=${token}; HttpOnly; Path=/; SameSite=Lax; Max-Age=28800`);
return res.status(200).json({ ok: true });
}
if (req.method === "DELETE") {
res.setHeader("Set-Cookie", `${COOKIE_NAME}=; HttpOnly; Path=/; Max-Age=0`);
return res.status(200).json({ ok: true });
}
if (req.method === "GET") {
const token = getTokenFromRequest(req);
const valid = token ? await verifyAdminToken(token) : false;
return res.status(200).json({ authenticated: valid });
}
res.setHeader("Allow", ["GET", "POST", "DELETE"]);
return res.status(405).json({ error: "Method Not Allowed" });
};
export default handler;

View File

@@ -0,0 +1,42 @@
import { NextApiHandler } from "next";
import { requireAdmin } from "@utils/admin-auth";
import { readSettings, writeSettings } from "@utils/settings-store";
const handler: NextApiHandler = async (req, res) => {
if (!(await requireAdmin(req, res))) return;
if (req.method === "GET") {
const settings = readSettings();
// Never expose the adminPasswordHash
const { adminPasswordHash: _omit, ...safe } = settings;
return res.status(200).json(safe);
}
if (req.method === "POST") {
const {
replicateApiToken,
jigsawApiKey,
modelVersion,
replicateEnabled,
newPassword
} = req.body ?? {};
const updates: Parameters<typeof writeSettings>[0] = {};
if (replicateApiToken !== undefined) updates.replicateApiToken = replicateApiToken;
if (jigsawApiKey !== undefined) updates.jigsawApiKey = jigsawApiKey;
if (modelVersion !== undefined) updates.modelVersion = modelVersion;
if (replicateEnabled !== undefined) updates.replicateEnabled = Boolean(replicateEnabled);
if (newPassword && typeof newPassword === "string" && newPassword.length >= 6) {
updates.adminPasswordHash = newPassword;
}
const saved = writeSettings(updates);
const { adminPasswordHash: _omit, ...safe } = saved;
return res.status(200).json(safe);
}
res.setHeader("Allow", ["GET", "POST"]);
return res.status(405).json({ error: "Method Not Allowed" });
};
export default handler;