Files
remoteconn-gitea/apps/gateway/src/sync/routes.ts
2026-03-21 18:57:10 +08:00

215 lines
6.8 KiB
TypeScript

import type { Express, Request, Response, NextFunction } from "express";
import { config } from "../config";
import { logger } from "../logger";
import { verifySyncToken } from "./crypto";
import { SyncRepository } from "./repository";
import {
syncLoginBodySchema,
syncRecordsPayloadSchema,
syncSettingsPayloadSchema,
syncServersPayloadSchema
} from "./schema";
import { loginMiniprogramUser } from "./userService";
import { registerMiniprogramTtsRoutes } from "../tts/routes";
interface SyncAuthedRequest extends Request {
syncUser?: {
userId: string;
openid: string;
};
}
function ensureSyncEnabled(res: Response): boolean {
if (config.sync.enabled) {
return true;
}
logger.warn(
{
hasAppId: Boolean(config.sync.miniprogramAppId),
hasAppSecret: Boolean(config.sync.miniprogramAppSecret),
hasSecretCurrent: Boolean(config.sync.secretCurrent)
},
"小程序同步服务未启用"
);
res.status(503).json({
ok: false,
code: "SYNC_DISABLED",
message: "小程序同步服务未配置"
});
return false;
}
function requireGatewayToken(req: Request, res: Response): boolean {
const token = String(req.headers["x-gateway-token"] || "");
if (token === config.gatewayToken) {
return true;
}
logger.warn(
{
path: req.path,
hasToken: Boolean(token)
},
"小程序同步登录缺少有效 gateway token"
);
res.status(401).json({
ok: false,
code: "AUTH_FAILED",
message: "gateway token 无效"
});
return false;
}
function requireSyncUser(req: SyncAuthedRequest, res: Response, next: NextFunction): void {
if (!ensureSyncEnabled(res)) return;
const auth = String(req.headers.authorization || "");
const token = auth.startsWith("Bearer ") ? auth.slice("Bearer ".length).trim() : "";
if (!token) {
logger.warn({ path: req.path }, "小程序同步请求缺少 Bearer token");
res.status(401).json({ ok: false, code: "SYNC_TOKEN_MISSING", message: "缺少同步令牌" });
return;
}
try {
const payload = verifySyncToken(token);
req.syncUser = {
userId: payload.uid,
openid: payload.oid
};
next();
} catch (error) {
logger.warn(
{
path: req.path,
err: error
},
"小程序同步请求鉴权失败"
);
res.status(401).json({
ok: false,
code: "SYNC_TOKEN_INVALID",
message: error instanceof Error ? error.message : "同步令牌无效"
});
}
}
export function registerSyncRoutes(app: Express): void {
const repository = new SyncRepository();
registerMiniprogramTtsRoutes(app, requireSyncUser);
app.post("/api/miniprogram/auth/login", async (req, res) => {
if (!ensureSyncEnabled(res) || !requireGatewayToken(req, res)) return;
const parsed = syncLoginBodySchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
ok: false,
code: "INVALID_BODY",
message: "登录参数不合法"
});
return;
}
try {
const result = await loginMiniprogramUser(parsed.data.code, repository);
res.json({
ok: true,
token: result.token,
expiresAt: result.expiresAt,
user: {
id: result.user.id
}
});
} catch (error) {
logger.warn({ err: error }, "小程序同步登录失败");
res.status(502).json({
ok: false,
code: "WECHAT_LOGIN_FAILED",
message: error instanceof Error ? error.message : "微信登录失败"
});
}
});
app.get("/api/miniprogram/sync/bootstrap", requireSyncUser, (req: SyncAuthedRequest, res) => {
const userId = req.syncUser?.userId;
if (!userId) {
res.status(401).json({ ok: false, code: "SYNC_TOKEN_INVALID", message: "同步令牌无效" });
return;
}
res.json({
ok: true,
settings: repository.getSettings(userId),
servers: repository.listServers(userId),
records: repository.listRecords(userId)
});
});
app.get("/api/miniprogram/sync/settings", requireSyncUser, (req: SyncAuthedRequest, res) => {
const userId = req.syncUser?.userId;
if (!userId) {
res.status(401).json({ ok: false, code: "SYNC_TOKEN_INVALID", message: "同步令牌无效" });
return;
}
res.json({ ok: true, settings: repository.getSettings(userId) });
});
app.put("/api/miniprogram/sync/settings", requireSyncUser, (req: SyncAuthedRequest, res) => {
const userId = req.syncUser?.userId;
if (!userId) {
res.status(401).json({ ok: false, code: "SYNC_TOKEN_INVALID", message: "同步令牌无效" });
return;
}
const parsed = syncSettingsPayloadSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ ok: false, code: "INVALID_BODY", message: "settings 参数不合法" });
return;
}
repository.upsertSettings(userId, parsed.data);
res.json({ ok: true, settings: repository.getSettings(userId) });
});
app.get("/api/miniprogram/sync/servers", requireSyncUser, (req: SyncAuthedRequest, res) => {
const userId = req.syncUser?.userId;
if (!userId) {
res.status(401).json({ ok: false, code: "SYNC_TOKEN_INVALID", message: "同步令牌无效" });
return;
}
res.json({ ok: true, servers: repository.listServers(userId) });
});
app.put("/api/miniprogram/sync/servers", requireSyncUser, (req: SyncAuthedRequest, res) => {
const userId = req.syncUser?.userId;
if (!userId) {
res.status(401).json({ ok: false, code: "SYNC_TOKEN_INVALID", message: "同步令牌无效" });
return;
}
const parsed = syncServersPayloadSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ ok: false, code: "INVALID_BODY", message: "servers 参数不合法" });
return;
}
repository.upsertServers(userId, parsed.data.servers);
res.json({ ok: true, servers: repository.listServers(userId) });
});
app.get("/api/miniprogram/sync/records", requireSyncUser, (req: SyncAuthedRequest, res) => {
const userId = req.syncUser?.userId;
if (!userId) {
res.status(401).json({ ok: false, code: "SYNC_TOKEN_INVALID", message: "同步令牌无效" });
return;
}
res.json({ ok: true, records: repository.listRecords(userId) });
});
app.put("/api/miniprogram/sync/records", requireSyncUser, (req: SyncAuthedRequest, res) => {
const userId = req.syncUser?.userId;
if (!userId) {
res.status(401).json({ ok: false, code: "SYNC_TOKEN_INVALID", message: "同步令牌无效" });
return;
}
const parsed = syncRecordsPayloadSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ ok: false, code: "INVALID_BODY", message: "records 参数不合法" });
return;
}
repository.upsertRecords(userId, parsed.data.records);
res.json({ ok: true, records: repository.listRecords(userId) });
});
}