9ecfa73831
Move admin backend into this repo (Express + MySQL on :8085) so npm run dev starts both API and Vite, with updated env defaults and Caddy config for gadm.tkmind.cn. Co-authored-by: Cursor <cursoragent@cursor.com>
376 lines
14 KiB
JavaScript
376 lines
14 KiB
JavaScript
import express from 'express';
|
|
import { paginateArray, listUsagePaged, listLedgerPaged } from './pagination.mjs';
|
|
|
|
export function createAdminApp(services) {
|
|
const { userAuth, llmProviderService, pool, ready } = services;
|
|
const app = express();
|
|
app.set('trust proxy', 1);
|
|
|
|
const isSecureRequest = (req) =>
|
|
req.secure || req.get('x-forwarded-proto')?.split(',')[0]?.trim() === 'https';
|
|
const jsonBody = express.json({ limit: '1mb' });
|
|
|
|
let cookieHelpers = null;
|
|
const cookieReady = ready.then(async () => {
|
|
const [userAuthMod, authMod] = await Promise.all([
|
|
services.importUserAuthModule(),
|
|
services.importAuthModule(),
|
|
]);
|
|
cookieHelpers = { ...userAuthMod, parseCookies: authMod.parseCookies };
|
|
});
|
|
|
|
function userToken(req) {
|
|
const { parseCookies, USER_COOKIE } = cookieHelpers;
|
|
return parseCookies(req.get('cookie'))[USER_COOKIE];
|
|
}
|
|
|
|
function setUserLoginCookies(res, req, token) {
|
|
const { userLoginCookies, resolveCookieDomainForRequest } = cookieHelpers;
|
|
res.set(
|
|
'Set-Cookie',
|
|
userLoginCookies(token, isSecureRequest(req), resolveCookieDomainForRequest(req)),
|
|
);
|
|
}
|
|
|
|
function clearUserLoginCookies(res, req) {
|
|
const { clearUserSessionCookie, resolveCookieDomainForRequest } = cookieHelpers;
|
|
res.set(
|
|
'Set-Cookie',
|
|
clearUserSessionCookie(isSecureRequest(req), resolveCookieDomainForRequest(req)),
|
|
);
|
|
}
|
|
|
|
app.get('/health', (_req, res) => {
|
|
res.json({ ok: true, service: 'memind_adm' });
|
|
});
|
|
|
|
app.get('/auth/status', async (req, res) => {
|
|
await cookieReady;
|
|
await ready;
|
|
const me = await userAuth.getMe(userToken(req));
|
|
if (!me) return res.json({ authenticated: false, mode: 'user' });
|
|
return res.json({ authenticated: true, user: me, mode: 'user' });
|
|
});
|
|
|
|
app.post('/auth/login', jsonBody, async (req, res) => {
|
|
await cookieReady;
|
|
await ready;
|
|
const { username, password } = req.body ?? {};
|
|
if (!username || !password) {
|
|
return res.status(400).json({ message: '用户名和密码不能为空' });
|
|
}
|
|
const result = await userAuth.login({ username, password, ip: req.ip });
|
|
if (!result.ok) {
|
|
if (result.retryAfterMs > 0) {
|
|
res.set('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));
|
|
return res.status(429).json({ message: result.message });
|
|
}
|
|
return res.status(401).json({ message: result.message });
|
|
}
|
|
setUserLoginCookies(res, req, result.token);
|
|
return res.json({ authenticated: true, user: result.user, mode: 'user' });
|
|
});
|
|
|
|
app.post('/auth/logout', async (req, res) => {
|
|
await cookieReady;
|
|
await ready;
|
|
await userAuth.revoke(userToken(req));
|
|
clearUserLoginCookies(res, req);
|
|
res.status(204).end();
|
|
});
|
|
|
|
const adminApi = express.Router();
|
|
adminApi.use(jsonBody);
|
|
|
|
adminApi.use(async (req, res, next) => {
|
|
await cookieReady;
|
|
await ready;
|
|
const me = await userAuth.getMe(userToken(req));
|
|
if (!me) return res.status(401).json({ message: '未登录' });
|
|
req.currentUser = me;
|
|
next();
|
|
});
|
|
|
|
const requireAdmin = (req, res, next) => {
|
|
if (!req.currentUser || req.currentUser.role !== 'admin') {
|
|
res.status(403).json({ message: '需要管理员权限' });
|
|
return;
|
|
}
|
|
next();
|
|
};
|
|
|
|
adminApi.get('/users', requireAdmin, async (req, res) => {
|
|
let users = await userAuth.listUsers();
|
|
const search = typeof req.query.search === 'string' ? req.query.search.trim().toLowerCase() : '';
|
|
const role = typeof req.query.role === 'string' ? req.query.role.trim() : '';
|
|
const status = typeof req.query.status === 'string' ? req.query.status.trim() : '';
|
|
if (search) {
|
|
users = users.filter(
|
|
(user) =>
|
|
user.username?.toLowerCase().includes(search) ||
|
|
user.displayName?.toLowerCase().includes(search) ||
|
|
user.email?.toLowerCase().includes(search),
|
|
);
|
|
}
|
|
if (role) users = users.filter((user) => user.role === role);
|
|
if (status) users = users.filter((user) => user.status === status);
|
|
const paged = paginateArray(users, req.query, 20);
|
|
res.json({ users: paged.items, total: paged.total, page: paged.page, pageSize: paged.pageSize });
|
|
});
|
|
|
|
adminApi.get('/summary', requireAdmin, async (_req, res) => {
|
|
const summary = await userAuth.getAdminSummary();
|
|
let llm = null;
|
|
if (llmProviderService) {
|
|
const keys = await llmProviderService.listKeys();
|
|
const global = await llmProviderService.getGlobalSettings();
|
|
const selected = keys.find((key) => key.isSelected);
|
|
llm = {
|
|
keyCount: keys.length,
|
|
selectedKeyName: selected?.name ?? null,
|
|
globalModel: global?.model ?? null,
|
|
};
|
|
}
|
|
res.json({ summary: { ...summary, llm } });
|
|
});
|
|
|
|
adminApi.post('/users', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.createUser(req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.status(201).json({ user: result.user });
|
|
});
|
|
|
|
adminApi.patch('/users/:userId', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.updateUser(req.params.userId, req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ user: result.user });
|
|
});
|
|
|
|
adminApi.post('/users/:userId/recharge', requireAdmin, async (req, res) => {
|
|
const amountCents = Number(req.body?.amountCents);
|
|
const note = typeof req.body?.note === 'string' ? req.body.note : '';
|
|
const result = await userAuth.recharge(
|
|
req.params.userId,
|
|
amountCents,
|
|
req.currentUser.id,
|
|
note,
|
|
);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ user: result.user });
|
|
});
|
|
|
|
adminApi.get('/usage', requireAdmin, async (req, res) => {
|
|
const result = await listUsagePaged(pool, req.query);
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/ledger', requireAdmin, async (req, res) => {
|
|
const result = await listLedgerPaged(pool, req.query);
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/capabilities/catalog', requireAdmin, (_req, res) => {
|
|
res.json({ catalog: userAuth.capabilityCatalog });
|
|
});
|
|
|
|
adminApi.get('/capabilities/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
if (role === 'admin') {
|
|
return res.json({
|
|
role,
|
|
capabilities: Object.fromEntries(
|
|
userAuth.capabilityCatalog.map((item) => [item.key, true]),
|
|
),
|
|
unrestricted: true,
|
|
});
|
|
}
|
|
res.json(await userAuth.getRoleCapabilities('user'));
|
|
});
|
|
|
|
adminApi.put('/capabilities/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
const result = await userAuth.setRoleCapabilities(role, req.body?.capabilities ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/users/:userId/capabilities', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.getUserCapabilities(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.put('/users/:userId/capabilities', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.setUserCapabilities(
|
|
req.params.userId,
|
|
req.body?.capabilities ?? {},
|
|
);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.delete('/users/:userId/capabilities', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.clearUserCapabilityOverrides(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/policies/catalog', requireAdmin, (_req, res) => {
|
|
res.json({ catalog: userAuth.policyCatalog });
|
|
});
|
|
|
|
adminApi.get('/policies/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
if (role === 'admin') {
|
|
return res.json({ role, policies: {}, unrestricted: true });
|
|
}
|
|
res.json(await userAuth.getRolePolicies('user'));
|
|
});
|
|
|
|
adminApi.put('/policies/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
const result = await userAuth.setRolePolicies(role, req.body?.policies ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/users/:userId/policies', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.getUserPolicies(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.put('/users/:userId/policies', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.setUserPolicies(req.params.userId, req.body?.policies ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.delete('/users/:userId/policies', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.clearUserPolicyOverrides(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/skills/catalog', requireAdmin, (_req, res) => {
|
|
res.json({ catalog: userAuth.skillCatalog });
|
|
});
|
|
|
|
adminApi.get('/skills/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
if (role === 'admin') {
|
|
return res.json({
|
|
role,
|
|
skills: Object.fromEntries(userAuth.skillCatalog.map((item) => [item.name, true])),
|
|
});
|
|
}
|
|
res.json(await userAuth.getRoleSkills('user'));
|
|
});
|
|
|
|
adminApi.put('/skills/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
const result = await userAuth.setRoleSkills(role, req.body?.skills ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/users/:userId/skills', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.getUserSkills(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.put('/users/:userId/skills', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.setUserSkills(req.params.userId, req.body?.skills ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.delete('/users/:userId/skills', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.clearUserSkillOverrides(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/llm-providers/catalog', requireAdmin, (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
res.json({ catalog: llmProviderService.catalog });
|
|
});
|
|
|
|
adminApi.get('/llm-providers/keys', requireAdmin, async (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
res.json({ keys: await llmProviderService.listKeys() });
|
|
});
|
|
|
|
adminApi.post('/llm-providers/keys', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.createKey(req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.status(201).json({ key: result.key });
|
|
});
|
|
|
|
adminApi.patch('/llm-providers/keys/:keyId', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.updateKey(req.params.keyId, req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ key: result.key });
|
|
});
|
|
|
|
adminApi.post('/llm-providers/keys/:keyId/select', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.selectKey(req.params.keyId);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ key: result.key });
|
|
});
|
|
|
|
adminApi.delete('/llm-providers/keys/:keyId', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.deleteKey(req.params.keyId);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
adminApi.post('/llm-providers/sync', requireAdmin, async (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
try {
|
|
res.json(await llmProviderService.syncSelectedToGoosed());
|
|
} catch (err) {
|
|
res.status(500).json({ message: err instanceof Error ? err.message : '同步失败' });
|
|
}
|
|
});
|
|
|
|
adminApi.get('/llm-providers/global', requireAdmin, async (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
res.json({ global: await llmProviderService.getGlobalSettings() });
|
|
});
|
|
|
|
adminApi.put('/llm-providers/global', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.setGlobalModel(req.body?.model);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.post('/llm-providers/test', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
try {
|
|
res.json(await llmProviderService.testDraft(req.body ?? {}));
|
|
} catch (err) {
|
|
res.status(500).json({ ok: false, message: err instanceof Error ? err.message : '联通测试失败' });
|
|
}
|
|
});
|
|
|
|
adminApi.post('/llm-providers/keys/:keyId/test', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
try {
|
|
res.json(await llmProviderService.testKey(req.params.keyId, req.body?.model));
|
|
} catch (err) {
|
|
res.status(500).json({ ok: false, message: err instanceof Error ? err.message : '联通测试失败' });
|
|
}
|
|
});
|
|
|
|
app.use('/admin-api', adminApi);
|
|
|
|
return app;
|
|
}
|