215 lines
6.8 KiB
TypeScript
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) });
|
|
});
|
|
}
|