fix: harden launch server runtime and public config

This commit is contained in:
stringadmin
2026-06-04 18:58:45 +08:00
parent 1a5992845a
commit df5ea8c65e
14 changed files with 926 additions and 32 deletions
+9 -9
View File
@@ -1495,7 +1495,7 @@ function registerAiRoutes(router) {
res.flushHeaders();
const abortController = new AbortController();
const streamTimer = setTimeout(() => abortController.abort(), 60000);
const streamTimer = setTimeout(() => abortController.abort(), 120000);
req.on("close", () => { clearTimeout(streamTimer); abortController.abort(); });
try {
@@ -1554,7 +1554,7 @@ function registerAiRoutes(router) {
}
} else {
const nonStreamAbort = new AbortController();
const nonStreamTimer = setTimeout(() => nonStreamAbort.abort(), 60000);
const nonStreamTimer = setTimeout(() => nonStreamAbort.abort(), 120000);
const upstream = await fetch(url, { method: "POST", headers: reqHeaders, body: reqBody, signal: nonStreamAbort.signal });
clearTimeout(nonStreamTimer);
const text = await upstream.text().catch(() => "");
@@ -1578,7 +1578,7 @@ function registerAiRoutes(router) {
const fallbackHeaders = { "Content-Type": "application/json", Authorization: "Bearer " + slotResult.apiKey };
const fallbackBody = JSON.stringify({ model: fallbackConfig.model, messages, stream: false, temperature: temperature || 0.7, max_tokens: 4096 });
const fbAbort = new AbortController();
const fbTimer = setTimeout(() => fbAbort.abort(), 60000);
const fbTimer = setTimeout(() => fbAbort.abort(), 90000);
const fbUpstream = await fetch(fallbackUrl, { method: "POST", headers: fallbackHeaders, body: fallbackBody, signal: fbAbort.signal });
clearTimeout(fbTimer);
const fbText = await fbUpstream.text().catch(() => "");
@@ -1609,7 +1609,7 @@ function registerAiRoutes(router) {
} catch (err) {
releaseLease(slotResult);
console.error("[ai/chat] error:", err.message);
res.status(500).json({ error: err.message });
res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
}
});
@@ -1690,7 +1690,7 @@ function registerAiRoutes(router) {
res.json({ taskId: String(rows[0].id), conversationId: rows[0].conversation_id });
} catch (err) {
res.status(500).json({ error: err.message });
res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
}
});
@@ -1714,7 +1714,7 @@ function registerAiRoutes(router) {
res.json(formatAiTaskRow(rows[0]));
} catch (err) {
res.status(500).json({ error: err.message });
res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
}
});
@@ -1761,7 +1761,7 @@ function registerAiRoutes(router) {
taskEvents.off(`task:${taskId}`, onUpdate);
});
} catch (err) {
if (!res.headersSent) res.status(500).json({ error: err.message });
if (!res.headersSent) res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
}
});
@@ -1826,7 +1826,7 @@ function registerAiRoutes(router) {
res.end(buffer);
} catch (err) {
console.error("[ai/tasks/download] failed:", err.message);
if (!res.headersSent) res.status(500).json({ error: err.message });
if (!res.headersSent) res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
}
});
@@ -1854,7 +1854,7 @@ function registerAiRoutes(router) {
res.end(buffer);
} catch (err) {
console.error("[ai/proxy-download] failed:", err.message);
if (!res.headersSent) res.status(500).json({ error: err.message });
if (!res.headersSent) res.status(err.name === "AbortError" ? 504 : 500).json({ error: err.name === "AbortError" ? "AI 上游响应超时,请重试" : err.message });
}
});
}
+146
View File
@@ -30,6 +30,13 @@ const {
buildOssPublicUrl,
normalizeAvatarOssKey,
normalizeProfileMediaUrl,
EMAIL_PURPOSES,
EMAIL_CODE_TTL_MINUTES,
EMAIL_CODE_COOLDOWN_SECONDS,
EMAIL_CODE_MAX_ATTEMPTS,
hashEmailCode,
sendEmailCode,
consumeEmailCode,
} = require("./context");
const {
checkBetaInviteCodeForRegistration,
@@ -208,15 +215,20 @@ function registerAuthRoutes(router) {
const email = normalizeEmail(req.body?.email);
const usernameInput = String(req.body?.username || "").trim();
const password = String(req.body?.password || "");
const code = String(req.body?.code || "").trim();
const emailError = validateEmail(email);
if (emailError) return res.status(400).json({ error: emailError });
if (!code) return res.status(400).json({ error: "缺少验证码" });
const passwordError = validatePassword(password);
if (passwordError) return res.status(400).json({ error: passwordError });
const registrationInvite = await ensureRegistrationInvite(req, res);
if (!registrationInvite) return;
try {
const verified = await consumeEmailCode(email, code, "register");
if (!verified) return res.status(400).json({ error: "验证码错误或已过期" });
const { rows: existingEmail } = await pool.query(
"SELECT id FROM users WHERE LOWER(email) = LOWER($1) LIMIT 1",
[email],
@@ -751,6 +763,140 @@ function registerAuthRoutes(router) {
res.status(500).json({ error: "更新个人资料失败" });
}
});
// ============================================================
// Email verification routes
// ============================================================
router.post("/auth/email/send-code", async (req, res) => {
const email = normalizeEmail(req.body?.email);
const purpose = String(req.body?.purpose || "register");
const emailError = validateEmail(email);
if (emailError) return res.status(400).json({ error: emailError });
if (!EMAIL_PURPOSES.has(purpose)) return res.status(400).json({ error: "验证码用途无效" });
if (purpose === "register") {
const inviteOk = await ensureBetaInviteCode(req, res);
if (!inviteOk) return;
}
try {
const { rows: recentCodes } = await pool.query(
"SELECT created_at FROM email_verification_codes WHERE email = $1 AND purpose = $2 AND created_at > NOW() - ($3::text || ' seconds')::interval ORDER BY created_at DESC LIMIT 1",
[email, purpose, EMAIL_CODE_COOLDOWN_SECONDS]
);
if (recentCodes.length > 0) {
return res.status(429).json({ error: "验证码发送太频繁,请 " + EMAIL_CODE_COOLDOWN_SECONDS + " 秒后再试" });
}
if (purpose === "register") {
const { rows: existing } = await pool.query("SELECT id FROM users WHERE LOWER(email) = LOWER($1) LIMIT 1", [email]);
if (existing.length > 0) return res.status(409).json({ error: "该邮箱已注册" });
}
if (purpose === "login" || purpose === "reset") {
const { rows: existing } = await pool.query("SELECT id FROM users WHERE LOWER(email) = LOWER($1) AND enabled = 1 LIMIT 1", [email]);
if (existing.length === 0) return res.status(404).json({ error: "该邮箱尚未注册" });
}
const code = generateSmsCode();
const codeHash = hashEmailCode(email, code);
await pool.query(
"INSERT INTO email_verification_codes (email, purpose, code_hash, expires_at) VALUES ($1, $2, $3, NOW() + ($4::text || ' minutes')::interval)",
[email, purpose, codeHash, EMAIL_CODE_TTL_MINUTES]
);
const sendResult = await sendEmailCode(email, code, purpose);
res.json({
success: true,
provider: sendResult.provider,
ttlSeconds: EMAIL_CODE_TTL_MINUTES * 60,
cooldownSeconds: EMAIL_CODE_COOLDOWN_SECONDS,
...(sendResult.devCode ? { devCode: sendResult.devCode } : {}),
});
} catch (error) {
console.error("[auth/email/send-code] failed", error);
res.status(500).json({ error: "验证码发送失败" });
}
});
router.post("/auth/email/verify", async (req, res) => {
const email = normalizeEmail(req.body?.email);
const code = String(req.body?.code || "").trim();
const purpose = String(req.body?.purpose || "register");
const emailError = validateEmail(email);
if (emailError) return res.status(400).json({ error: emailError });
if (!code) return res.status(400).json({ error: "缺少验证码" });
if (!EMAIL_PURPOSES.has(purpose)) return res.status(400).json({ error: "验证码用途无效" });
try {
const verified = await consumeEmailCode(email, code, purpose);
if (!verified) return res.status(400).json({ error: "验证码错误或已过期" });
if (purpose === "register" || purpose === "login") {
await pool.query("UPDATE users SET email_verified = 1 WHERE LOWER(email) = LOWER($1)", [email]);
}
res.json({ success: true });
} catch (error) {
console.error("[auth/email/verify] failed", error);
res.status(500).json({ error: "验证失败" });
}
});
router.post("/auth/forgot-password", async (req, res) => {
const email = normalizeEmail(req.body?.email);
const emailError = validateEmail(email);
if (emailError) return res.status(400).json({ error: emailError });
try {
const { rows } = await pool.query("SELECT id FROM users WHERE LOWER(email) = LOWER($1) AND enabled = 1 LIMIT 1", [email]);
if (rows.length === 0) {
return res.json({ success: true, message: "如果该邮箱已注册,重置链接已发送" });
}
const { rows: recentCodes } = await pool.query(
"SELECT created_at FROM email_verification_codes WHERE email = $1 AND purpose = 'reset' AND created_at > NOW() - ($2::text || ' seconds')::interval ORDER BY created_at DESC LIMIT 1",
[email, EMAIL_CODE_COOLDOWN_SECONDS]
);
if (recentCodes.length > 0) {
return res.status(429).json({ error: "发送太频繁,请 " + EMAIL_CODE_COOLDOWN_SECONDS + " 秒后再试" });
}
const code = generateSmsCode();
const codeHash = hashEmailCode(email, code);
await pool.query(
"INSERT INTO email_verification_codes (email, purpose, code_hash, expires_at) VALUES ($1, 'reset', $2, NOW() + ($3::text || ' minutes')::interval)",
[email, codeHash, EMAIL_CODE_TTL_MINUTES]
);
await sendEmailCode(email, code, "reset");
res.json({ success: true, message: "重置验证码已发送到您的邮箱" });
} catch (error) {
console.error("[auth/forgot-password] failed", error);
res.status(500).json({ error: "发送失败" });
}
});
router.post("/auth/reset-password", async (req, res) => {
const email = normalizeEmail(req.body?.email);
const code = String(req.body?.code || "").trim();
const newPassword = String(req.body?.newPassword || "");
const emailError = validateEmail(email);
if (emailError) return res.status(400).json({ error: emailError });
if (!code) return res.status(400).json({ error: "缺少验证码" });
const passwordError = validatePassword(newPassword);
if (passwordError) return res.status(400).json({ error: passwordError });
try {
const verified = await consumeEmailCode(email, code, "reset");
if (!verified) return res.status(400).json({ error: "验证码错误或已过期" });
const hash = await bcrypt.hash(newPassword, 10);
await pool.query("UPDATE users SET password_hash = $1 WHERE LOWER(email) = LOWER($2)", [hash, email]);
res.json({ success: true, message: "密码重置成功,请重新登录" });
} catch (error) {
console.error("[auth/reset-password] failed", error);
res.status(500).json({ error: "密码重置失败" });
}
});
}
module.exports = {
+71
View File
@@ -53,6 +53,10 @@ const SMS_PURPOSES = new Set(["register", "login"]);
const SMS_CODE_TTL_MINUTES = Math.max(1, Number(process.env.SMS_CODE_TTL_MINUTES) || 10);
const SMS_CODE_COOLDOWN_SECONDS = Math.max(10, Number(process.env.SMS_CODE_COOLDOWN_SECONDS) || 60);
const SMS_CODE_MAX_ATTEMPTS = Math.max(1, Number(process.env.SMS_CODE_MAX_ATTEMPTS) || 5);
const EMAIL_PURPOSES = new Set(["register", "login", "reset"]);
const EMAIL_CODE_TTL_MINUTES = Math.max(1, Number(process.env.EMAIL_CODE_TTL_MINUTES) || 10);
const EMAIL_CODE_COOLDOWN_SECONDS = Math.max(10, Number(process.env.EMAIL_CODE_COOLDOWN_SECONDS) || 60);
const EMAIL_CODE_MAX_ATTEMPTS = Math.max(1, Number(process.env.EMAIL_CODE_MAX_ATTEMPTS) || 5);
function validateUsername(username) {
if (!username) return "缺少用户名";
@@ -201,6 +205,66 @@ async function consumeSmsCode(phone, code, purpose) {
await pool.query("UPDATE sms_verification_codes SET consumed_at = NOW() WHERE id = $1", [row.id]);
return true;
}
function hashEmailCode(email, code) {
const secret = process.env.EMAIL_CODE_SECRET || process.env.JWT_SECRET || "omniai-dev-email-secret";
return crypto.createHash("sha256").update(email + ":" + code + ":" + secret).digest("hex");
}
async function sendEmailCode(email, code, purpose) {
const provider = String(process.env.EMAIL_PROVIDER || "mock").trim().toLowerCase();
if (provider === "smtp") {
const nodemailer = require("nodemailer");
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === "1",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
const purposeText = purpose === "register" ? "注册" : purpose === "login" ? "登录" : "重置密码";
await transporter.sendMail({
from: process.env.SMTP_FROM || process.env.SMTP_USER,
to: email,
subject: "[OmniAI] \u90ae\u7bb1\u9a8c\u8bc1\u7801",
text: "\u60a8\u7684\u9a8c\u8bc1\u7801\u662f\uff1a" + code + "\n\u7528\u9014\uff1a" + purposeText + "\n\u6709\u6548\u671f\uff1a" + String(process.env.EMAIL_CODE_TTL_MINUTES || 10) + " \u5206\u949f\n\u5982\u679c\u4e0d\u662f\u60a8\u672c\u4eba\u64cd\u4f5c\uff0c\u8bf7\u5ffd\u7565\u6b64\u90ae\u4ef6\u3002",
html: "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><h2 style=\"color:#333\">OmniAI \u90ae\u7bb1\u9a8c\u8bc1</h2><p style=\"font-size:16px;color:#555\">\u60a8\u7684\u9a8c\u8bc1\u7801\u662f\uff1a</p><p style=\"font-size:32px;font-weight:bold;letter-spacing:6px;color:#1677ff;margin:16px 0\">" + code + "</p><p style=\"color:#888\">\u7528\u9014\uff1a" + purposeText + "</p><p style=\"color:#888\">\u6709\u6548\u671f\uff1a" + String(process.env.EMAIL_CODE_TTL_MINUTES || 10) + " \u5206\u949f</p><hr style=\"border:none;border-top:1px solid #eee;margin:24px 0\"><p style=\"color:#aaa;font-size:13px\">\u5982\u679c\u4e0d\u662f\u60a8\u672c\u4eba\u64cd\u4f5c\uff0c\u8bf7\u5ffd\u7565\u6b64\u90ae\u4ef6\u3002</p></div>",
});
return { provider: "smtp" };
}
console.log("[email:" + purpose + "] " + email + " verification code: " + code + " (mock provider)");
return {
provider: "mock",
devCode: process.env.EMAIL_DEV_RETURN_CODE === "1" ? code : undefined,
};
}
async function consumeEmailCode(email, code, purpose) {
const { rows } = await pool.query(
"SELECT id, code_hash, attempts FROM email_verification_codes WHERE email = $1 AND purpose = $2 AND consumed_at IS NULL AND expires_at > NOW() ORDER BY created_at DESC LIMIT 1",
[email, purpose]
);
const row = rows[0];
if (!row) return false;
if (Number(row.attempts || 0) >= EMAIL_CODE_MAX_ATTEMPTS) {
return false;
}
const expectedHash = hashEmailCode(email, String(code || "").trim());
if (row.code_hash !== expectedHash) {
await pool.query("UPDATE email_verification_codes SET attempts = attempts + 1 WHERE id = $1", [row.id]);
return false;
}
await pool.query("UPDATE email_verification_codes SET consumed_at = NOW() WHERE id = $1", [row.id]);
return true;
}
function getWechatLoginConfig() {
const appId = process.env.WECHAT_LOGIN_APP_ID || process.env.WECHAT_APP_ID || "";
@@ -742,6 +806,10 @@ module.exports = {
PRICE_TYPES,
PHONE_PATTERN,
EMAIL_PATTERN,
EMAIL_PURPOSES,
EMAIL_CODE_TTL_MINUTES,
EMAIL_CODE_COOLDOWN_SECONDS,
EMAIL_CODE_MAX_ATTEMPTS,
SMS_PURPOSES,
SMS_CODE_TTL_MINUTES,
SMS_CODE_COOLDOWN_SECONDS,
@@ -755,6 +823,9 @@ module.exports = {
hashSmsCode,
generateSmsCode,
sendSmsCode,
hashEmailCode,
sendEmailCode,
consumeEmailCode,
createLoginResultForUserId,
sanitizeUsernameSeed,
generateUniqueUsername,
+169
View File
@@ -131,3 +131,172 @@ function registerEcommerceRoutes(router) {
}
module.exports = { registerEcommerceRoutes };
function registerEcommerceHistoryRoutes(router) {
router.post("/ai/ecommerce/video-history", requireAuth, async (req, res) => {
const userId = req.user.id;
const { title, config, plan, scenes, sourceImageUrls } = req.body;
if (!scenes || !Array.isArray(scenes) || scenes.length === 0) {
return res.status(400).json({ error: "Missing scenes data" });
}
try {
const { rows } = await pool.query(
`INSERT INTO ecommerce_video_history (user_id, title, config_json, plan_json, scenes_json, source_image_urls)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, created_at`,
[
userId,
title || "",
JSON.stringify(config || {}),
JSON.stringify(plan || {}),
JSON.stringify(scenes),
JSON.stringify(sourceImageUrls || []),
]
);
res.json({ id: rows[0].id, createdAt: rows[0].created_at });
} catch (err) {
console.error("[ecommerce/video-history] save error:", err.message);
res.status(500).json({ error: "保存失败" });
}
});
router.get("/ai/ecommerce/video-history", requireAuth, async (req, res) => {
const userId = req.user.id;
const limit = Math.min(Math.max(Number(req.query.limit) || 20, 1), 100);
const offset = Math.max(Number(req.query.offset) || 0, 0);
try {
const { rows } = await pool.query(
`SELECT id, title, config_json, scenes_json, source_image_urls, created_at
FROM ecommerce_video_history
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3`,
[userId, limit, offset]
);
const { rows: countRows } = await pool.query(
"SELECT COUNT(*)::int AS total FROM ecommerce_video_history WHERE user_id = $1",
[userId]
);
res.json({
items: rows.map(r => ({
id: r.id,
title: r.title,
config: JSON.parse(r.config_json || "{}"),
scenes: JSON.parse(r.scenes_json || "[]"),
sourceImageUrls: JSON.parse(r.source_image_urls || "[]"),
createdAt: r.created_at,
})),
total: countRows[0].total,
limit,
offset,
});
} catch (err) {
console.error("[ecommerce/video-history] list error:", err.message);
res.status(500).json({ error: "查询失败" });
}
});
router.get("/ai/ecommerce/video-history/:id", requireAuth, async (req, res) => {
const userId = req.user.id;
const id = Number(req.params.id);
try {
const { rows } = await pool.query(
`SELECT id, title, config_json, plan_json, scenes_json, source_image_urls, created_at
FROM ecommerce_video_history
WHERE id = $1 AND user_id = $2`,
[id, userId]
);
if (rows.length === 0) {
return res.status(404).json({ error: "记录不存在" });
}
const r = rows[0];
res.json({
id: r.id,
title: r.title,
config: JSON.parse(r.config_json || "{}"),
plan: JSON.parse(r.plan_json || "{}"),
scenes: JSON.parse(r.scenes_json || "[]"),
sourceImageUrls: JSON.parse(r.source_image_urls || "[]"),
createdAt: r.created_at,
});
} catch (err) {
console.error("[ecommerce/video-history] get error:", err.message);
res.status(500).json({ error: "查询失败" });
}
});
router.delete("/ai/ecommerce/video-history/:id", requireAuth, async (req, res) => {
const userId = req.user.id;
const id = Number(req.params.id);
try {
// Fetch record first to get OSS URLs before deletion
const { rows } = await pool.query(
"SELECT scenes_json, source_image_urls FROM ecommerce_video_history WHERE id = $1 AND user_id = $2",
[id, userId]
);
if (rows.length === 0) {
return res.status(404).json({ error: "记录不存在" });
}
// Extract all OSS URLs from scenes and source images
const scenes = JSON.parse(rows[0].scenes_json || "[]");
const sourceUrls = JSON.parse(rows[0].source_image_urls || "[]");
const ossUrlsToDelete = [];
for (const scene of scenes) {
if (scene.imageUrl) ossUrlsToDelete.push(scene.imageUrl);
if (scene.videoUrl) ossUrlsToDelete.push(scene.videoUrl);
}
for (const url of sourceUrls) {
if (url) ossUrlsToDelete.push(url);
}
// Delete from database
await pool.query(
"DELETE FROM ecommerce_video_history WHERE id = $1 AND user_id = $2",
[id, userId]
);
// Delete OSS files in background (best-effort)
const { deleteObject, isOssConfigured } = require("../ossClient");
if (isOssConfigured() && ossUrlsToDelete.length > 0) {
const bucket = String(process.env.OSS_BUCKET || "").trim();
const region = String(process.env.OSS_REGION || "").trim().replace(/^oss-/, "");
const ossHost = bucket + ".oss-" + region + ".aliyuncs.com";
const publicBase = String(process.env.OSS_PUBLIC_BASE_URL || "").trim().replace(/\/+$/, "");
for (const url of ossUrlsToDelete) {
try {
let ossKey = "";
if (publicBase && url.startsWith(publicBase)) {
ossKey = url.slice(publicBase.length + 1);
} else if (url.includes(ossHost)) {
const parsed = new URL(url);
ossKey = decodeURIComponent(parsed.pathname.slice(1));
}
if (ossKey) {
await deleteObject(ossKey);
}
} catch (delErr) {
console.warn("[ecommerce/video-history] OSS delete failed for:", url, delErr.message);
}
}
}
res.json({ success: true });
} catch (err) {
console.error("[ecommerce/video-history] delete error:", err.message);
res.status(500).json({ error: "删除失败" });
}
});
}
module.exports.registerEcommerceHistoryRoutes = registerEcommerceHistoryRoutes;
+110
View File
@@ -0,0 +1,110 @@
"use strict";
const mammoth = require("mammoth");
const { requireAuth } = require("./context");
function registerFileExtractRoutes(router) {
router.post("/files/extract-text", requireAuth, async (req, res) => {
try {
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
const body = Buffer.concat(chunks);
const contentType = req.headers["content-type"] || "";
const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;\s]+))/);
if (!boundaryMatch) {
return res.status(400).json({ error: "Missing multipart boundary" });
}
const boundary = boundaryMatch[1] || boundaryMatch[2];
const parts = parseMultipart(body, boundary);
const filePart = parts.find(p => p.name === "file");
if (!filePart || !filePart.data || filePart.data.length === 0) {
return res.status(400).json({ error: "No file uploaded" });
}
const filename = filePart.filename || "unknown";
const ext = filename.lastIndexOf(".") >= 0
? filename.slice(filename.lastIndexOf(".")).toLowerCase()
: "";
let text = "";
if (ext === ".docx") {
const result = await mammoth.extractRawText({ buffer: filePart.data });
text = result.value || "";
} else if (ext === ".doc") {
text = filePart.data
.toString("utf-8")
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
.replace(/\s{3,}/g, "\n\n")
.trim();
} else {
text = filePart.data.toString("utf-8");
}
if (!text || text.trim().length === 0) {
return res.status(422).json({ error: "Unable to extract text" });
}
res.json({ text: text.trim(), filename });
} catch (err) {
console.error("[files/extract-text] error:", err.message);
res.status(500).json({ error: err.message });
}
});
}
function parseMultipart(body, boundary) {
const parts = [];
const boundaryBuf = Buffer.from("--" + boundary);
const crlf = Buffer.from("\r\n");
const doubleCrlf = Buffer.from("\r\n\r\n");
let start = bufIndexOf(body, boundaryBuf, 0);
if (start === -1) return parts;
start += boundaryBuf.length + crlf.length;
while (true) {
const end = bufIndexOf(body, boundaryBuf, start);
if (end === -1) break;
const partData = body.slice(start, end - crlf.length);
const headerEnd = bufIndexOf(partData, doubleCrlf, 0);
if (headerEnd === -1) {
start = end + boundaryBuf.length + crlf.length;
continue;
}
const headerStr = partData.slice(0, headerEnd).toString("utf-8");
const content = partData.slice(headerEnd + doubleCrlf.length);
const nameMatch = headerStr.match(/name="([^"]+)"/);
const filenameMatch = headerStr.match(/filename="([^"]*)"/);
parts.push({
name: nameMatch ? nameMatch[1] : "",
filename: filenameMatch ? filenameMatch[1] : null,
data: content,
});
const afterBoundary = body.slice(end + boundaryBuf.length, end + boundaryBuf.length + 2);
if (afterBoundary.toString() === "--") break;
start = end + boundaryBuf.length + crlf.length;
}
return parts;
}
function bufIndexOf(buf, search, from) {
for (let i = from; i <= buf.length - search.length; i++) {
let found = true;
for (let j = 0; j < search.length; j++) {
if (buf[i + j] !== search[j]) { found = false; break; }
}
if (found) return i;
}
return -1;
}
module.exports = { registerFileExtractRoutes };
+6 -2
View File
@@ -1,6 +1,6 @@
const express = require('express')
const { registerAuthRoutes } = require('./auth')
const { registerPriceRoutes, registerPackageRoutes, registerHealthRoutes } = require('./public')
const { registerPriceRoutes, registerPackageRoutes, registerPublicConfigRoutes, registerHealthRoutes } = require('./public')
const { registerKeyRoutes } = require('./keys')
const { registerAdminRoutes, registerAdminInvoiceRoutes } = require('./admin')
const { registerEnterpriseRoutes } = require('./enterprise')
@@ -12,18 +12,20 @@ const { registerProjectRoutes } = require('./projects')
const { registerOssRoutes } = require('./oss')
const { registerCommunityRoutes, registerAdminCommunityRoutes } = require('./community')
const { registerAiRoutes } = require('./ai')
const { registerEcommerceRoutes } = require("./ecommerce")
const { registerEcommerceRoutes, registerEcommerceHistoryRoutes } = require("./ecommerce")
const { registerConversationRoutes } = require('./conversations')
const { registerReportRoutes } = require('./reports')
const { registerAssetRoutes } = require('./assets')
const { registerNotificationRoutes } = require('./notifications')
const { registerDraftRoutes } = require('./drafts');
const { registerFileExtractRoutes } = require('./fileExtract');
const mountClientErrorRoutes = require('./clientErrors')
const router = express.Router()
registerAuthRoutes(router)
registerPriceRoutes(router)
registerPublicConfigRoutes(router)
registerKeyRoutes(router)
registerAdminRoutes(router)
registerPackageRoutes(router)
@@ -41,11 +43,13 @@ registerCommunityRoutes(router)
registerAdminCommunityRoutes(router)
registerAiRoutes(router)
registerEcommerceRoutes(router)
registerEcommerceHistoryRoutes(router)
registerConversationRoutes(router)
registerReportRoutes(router)
registerAssetRoutes(router)
registerNotificationRoutes(router)
registerDraftRoutes(router)
registerFileExtractRoutes(router)
registerHealthRoutes(router)
module.exports = router
+36
View File
@@ -36,14 +36,50 @@ function registerPackageRoutes(router) {
function registerHealthRoutes(router) {
// ── Health ───────────────────────────────────────────────────────────
// Public health: minimal response, no provider/key details exposed
router.get("/health", async (_req, res) => {
res.json({ status: "ok", uptime: process.uptime() });
});
// Admin-only health: full provider status (requires auth via admin middleware)
router.get("/admin/providers/status", async (_req, res) => {
const status = await keyManager.getAllStatus();
res.json({ status: "ok", uptime: process.uptime(), providers: status });
});
}
const PUBLIC_CONFIG_PROFILES = new Set(["web-public-config", "web-model-capabilities"]);
function createPublicConfigFallback(name) {
if (name === "web-model-capabilities") {
return {
imageModels: [],
videoModels: [],
chatModels: [],
};
}
return {};
}
function registerPublicConfigRoutes(router) {
router.get("/public/config/profile", async (req, res) => {
const name = String(req.query.name || "web-public-config").trim() || "web-public-config";
if (!PUBLIC_CONFIG_PROFILES.has(name)) return res.status(404).json({ error: "Public config profile not found" });
const { rows: [row] } = await pool.query(
"SELECT config_json, description, updated_at FROM config_profiles WHERE name = $1",
[name],
);
if (!row) return res.json({ name, config: createPublicConfigFallback(name), description: "", updatedAt: null });
let config = {};
try { config = JSON.parse(row.config_json || "{}"); } catch {}
return res.json({ name, config, description: row.description || "", updatedAt: row.updated_at });
});
}
module.exports = {
registerPriceRoutes,
registerPackageRoutes,
registerPublicConfigRoutes,
registerHealthRoutes,
};