348e192800
Wire /learn static delivery, parent/child portal routes, and Tang page build scripts for 103 deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
295 lines
10 KiB
JavaScript
295 lines
10 KiB
JavaScript
import path from 'node:path';
|
|
import { createLearnAssistantFamilyRegistry } from './learn-assistant-family.mjs';
|
|
import { createLearnAssistantService } from './learn-assistant-service.mjs';
|
|
|
|
function handleError(res, error) {
|
|
const status = error?.statusCode ?? 500;
|
|
const message = error instanceof Error ? error.message : '请求失败';
|
|
if (status >= 500) console.error('[LearnAssistant]', message);
|
|
return res.status(status).json({ error: message });
|
|
}
|
|
|
|
async function runHandler(res, work) {
|
|
try {
|
|
const body = await work;
|
|
return res.json(body);
|
|
} catch (error) {
|
|
return handleError(res, error);
|
|
}
|
|
}
|
|
|
|
function readLegacyPassword(req) {
|
|
return req.get('x-parent-pw') || req.get('x-admin-pw') || '';
|
|
}
|
|
|
|
function currentUser(req) {
|
|
return req.currentUser ?? null;
|
|
}
|
|
|
|
export function createLearnAssistantRuntime(options = {}) {
|
|
const dataRoot = options.dataRoot ?? path.join(options.rootDir ?? process.cwd(), 'data', 'learn-assistant');
|
|
const service = options.service ?? createLearnAssistantService({ dataRoot });
|
|
const familyRegistry = options.familyRegistry
|
|
?? createLearnAssistantFamilyRegistry({ dataRoot, seedFamilyDefaults: service.seedFamilyDefaults });
|
|
return { dataRoot, familyRegistry, service };
|
|
}
|
|
|
|
export function attachLearnAssistantPublicRoutes(api, options = {}) {
|
|
const { familyRegistry, service } = createLearnAssistantRuntime(options);
|
|
const prefix = options.routePrefix ?? '/learn';
|
|
|
|
async function withSlug(req, res, handler) {
|
|
try {
|
|
const family = await familyRegistry.getFamilyBySlug(req.params.slug);
|
|
return runHandler(res, handler(family));
|
|
} catch (error) {
|
|
return handleError(res, error);
|
|
}
|
|
}
|
|
|
|
api.get(`${prefix}/public/:slug/children`, (req, res) =>
|
|
withSlug(req, res, (family) => service.listChildren(family.id)));
|
|
|
|
api.get(`${prefix}/public/:slug/today`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.getToday(family.id, req.query.child, req.query.date)));
|
|
|
|
api.get(`${prefix}/public/:slug/day`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.getDay(family.id, req.query.child, req.query.date)));
|
|
|
|
api.post(`${prefix}/public/:slug/checkin`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.checkin(family.id, req.body?.id, req.body)));
|
|
|
|
api.get(`${prefix}/public/:slug/calendar`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.getCalendar(family.id, req.query.child, req.query.month)));
|
|
|
|
api.get(`${prefix}/public/:slug/growth`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.getGrowth(family.id, req.query.child)));
|
|
|
|
api.get(`${prefix}/public/:slug/history`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.getHistory(family.id, req.query.child)));
|
|
|
|
// legacy slug routes (tang 等旧链接)
|
|
api.get(`${prefix}/:slug/children`, (req, res) =>
|
|
withSlug(req, res, (family) => service.listChildren(family.id)));
|
|
|
|
api.get(`${prefix}/:slug/today`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.getToday(family.id, req.query.child, req.query.date)));
|
|
|
|
api.get(`${prefix}/:slug/day`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.getDay(family.id, req.query.child, req.query.date)));
|
|
|
|
api.post(`${prefix}/:slug/checkin`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.checkin(family.id, req.body?.id, req.body)));
|
|
|
|
api.get(`${prefix}/:slug/calendar`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.getCalendar(family.id, req.query.child, req.query.month)));
|
|
|
|
api.get(`${prefix}/:slug/growth`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.getGrowth(family.id, req.query.child)));
|
|
|
|
api.get(`${prefix}/:slug/history`, (req, res) =>
|
|
withSlug(req, res, (family) =>
|
|
service.getHistory(family.id, req.query.child)));
|
|
|
|
// legacy password parent API (tang 旧家长端)
|
|
async function withLegacyParent(req, res, handler) {
|
|
try {
|
|
const family = await familyRegistry.getFamilyBySlug(req.params.slug);
|
|
await service.assertLegacyPassword(family.id, readLegacyPassword(req));
|
|
return runHandler(res, handler(family));
|
|
} catch (error) {
|
|
return handleError(res, error);
|
|
}
|
|
}
|
|
|
|
api.get(`${prefix}/:slug/parent/stats`, (req, res) =>
|
|
withLegacyParent(req, res, (family) => service.parentStats(family.id)));
|
|
|
|
api.get(`${prefix}/:slug/parent/templates`, (req, res) =>
|
|
withLegacyParent(req, res, (family) => service.parentListTemplates(family.id)));
|
|
|
|
api.post(`${prefix}/:slug/parent/templates`, (req, res) =>
|
|
withLegacyParent(req, res, (family) =>
|
|
service.parentSaveTemplate(family.id, req.body)));
|
|
|
|
api.put(`${prefix}/:slug/parent/templates/:id`, (req, res) =>
|
|
withLegacyParent(req, res, (family) =>
|
|
service.parentSaveTemplate(family.id, req.body, +req.params.id)));
|
|
|
|
api.delete(`${prefix}/:slug/parent/templates/:id`, (req, res) =>
|
|
withLegacyParent(req, res, (family) =>
|
|
service.parentDeleteTemplate(family.id, +req.params.id)));
|
|
|
|
api.get(`${prefix}/:slug/parent/categories`, (req, res) =>
|
|
withLegacyParent(req, res, (family) => service.parentListCategories(family.id)));
|
|
|
|
api.post(`${prefix}/:slug/parent/categories`, (req, res) =>
|
|
withLegacyParent(req, res, (family) =>
|
|
service.parentAddCategory(family.id, req.body)));
|
|
|
|
api.delete(`${prefix}/:slug/parent/categories/:id`, (req, res) =>
|
|
withLegacyParent(req, res, (family) =>
|
|
service.parentDeleteCategory(family.id, +req.params.id)));
|
|
|
|
api.get(`${prefix}/:slug/parent/children`, (req, res) =>
|
|
withLegacyParent(req, res, (family) => service.parentListChildren(family.id)));
|
|
|
|
api.post(`${prefix}/:slug/parent/children`, (req, res) =>
|
|
withLegacyParent(req, res, (family) =>
|
|
service.parentAddChild(family.id, req.body)));
|
|
|
|
api.delete(`${prefix}/:slug/parent/children/:id`, (req, res) =>
|
|
withLegacyParent(req, res, (family) =>
|
|
service.parentDeleteChild(family.id, +req.params.id)));
|
|
|
|
api.post(`${prefix}/:slug/parent/password`, (req, res) =>
|
|
withLegacyParent(req, res, (family) =>
|
|
service.parentChangePassword(family.id, req.body)));
|
|
|
|
return { familyRegistry, service };
|
|
}
|
|
|
|
export function attachLearnAssistantParentRoutes(api, options = {}) {
|
|
const { familyRegistry, service } = createLearnAssistantRuntime(options);
|
|
const prefix = options.routePrefix ?? '/learn';
|
|
|
|
function requireUser(req, res) {
|
|
const user = currentUser(req);
|
|
if (!user?.id) {
|
|
res.status(401).json({ error: '请先登录 Memind' });
|
|
return null;
|
|
}
|
|
return user;
|
|
}
|
|
|
|
async function withFamily(req, res, handler) {
|
|
const user = requireUser(req, res);
|
|
if (!user) return null;
|
|
try {
|
|
const family = await familyRegistry.requireFamilyForUser(user.id, {
|
|
displayName: user.displayName ?? user.display_name ?? user.username,
|
|
});
|
|
return runHandler(res, handler(family, user));
|
|
} catch (error) {
|
|
return handleError(res, error);
|
|
}
|
|
}
|
|
|
|
api.get(`${prefix}/me/family`, async (req, res) => {
|
|
const user = requireUser(req, res);
|
|
if (!user) return;
|
|
try {
|
|
const { family, created } = await familyRegistry.getFamilyForUser(user.id, {
|
|
displayName: user.displayName ?? user.display_name ?? user.username,
|
|
});
|
|
const urls = familyRegistry.familyUrls(family);
|
|
return res.json({
|
|
family: {
|
|
id: family.id,
|
|
slug: family.slug,
|
|
name: family.name,
|
|
members: family.members,
|
|
},
|
|
role: family.members.find((m) => m.userId === user.id)?.role ?? 'parent',
|
|
urls,
|
|
created,
|
|
});
|
|
} catch (error) {
|
|
return handleError(res, error);
|
|
}
|
|
});
|
|
|
|
api.post(`${prefix}/me/invite`, (req, res) =>
|
|
withFamily(req, res, async (family, user) => {
|
|
const invite = await familyRegistry.createInvite(user.id, {
|
|
displayName: user.displayName ?? user.display_name ?? user.username,
|
|
});
|
|
const urls = familyRegistry.familyUrls(family);
|
|
return {
|
|
...invite,
|
|
joinUrl: `${urls.joinPath}${invite.token}`,
|
|
};
|
|
}));
|
|
|
|
api.post(`${prefix}/me/join`, async (req, res) => {
|
|
const user = requireUser(req, res);
|
|
if (!user) return;
|
|
try {
|
|
const result = await familyRegistry.acceptInvite(user.id, req.body?.token, {
|
|
displayName: user.displayName ?? user.display_name ?? user.username,
|
|
});
|
|
return res.json({
|
|
ok: true,
|
|
family: {
|
|
id: result.family.id,
|
|
slug: result.family.slug,
|
|
name: result.family.name,
|
|
members: result.family.members,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
return handleError(res, error);
|
|
}
|
|
});
|
|
|
|
api.get(`${prefix}/me/parent/stats`, (req, res) =>
|
|
withFamily(req, res, (family) => service.parentStats(family.id)));
|
|
|
|
api.get(`${prefix}/me/parent/templates`, (req, res) =>
|
|
withFamily(req, res, (family) => service.parentListTemplates(family.id)));
|
|
|
|
api.post(`${prefix}/me/parent/templates`, (req, res) =>
|
|
withFamily(req, res, (family) =>
|
|
service.parentSaveTemplate(family.id, req.body)));
|
|
|
|
api.put(`${prefix}/me/parent/templates/:id`, (req, res) =>
|
|
withFamily(req, res, (family) =>
|
|
service.parentSaveTemplate(family.id, req.body, +req.params.id)));
|
|
|
|
api.delete(`${prefix}/me/parent/templates/:id`, (req, res) =>
|
|
withFamily(req, res, (family) =>
|
|
service.parentDeleteTemplate(family.id, +req.params.id)));
|
|
|
|
api.get(`${prefix}/me/parent/categories`, (req, res) =>
|
|
withFamily(req, res, (family) => service.parentListCategories(family.id)));
|
|
|
|
api.post(`${prefix}/me/parent/categories`, (req, res) =>
|
|
withFamily(req, res, (family) =>
|
|
service.parentAddCategory(family.id, req.body)));
|
|
|
|
api.delete(`${prefix}/me/parent/categories/:id`, (req, res) =>
|
|
withFamily(req, res, (family) =>
|
|
service.parentDeleteCategory(family.id, +req.params.id)));
|
|
|
|
api.get(`${prefix}/me/parent/children`, (req, res) =>
|
|
withFamily(req, res, (family) => service.parentListChildren(family.id)));
|
|
|
|
api.post(`${prefix}/me/parent/children`, (req, res) =>
|
|
withFamily(req, res, (family) =>
|
|
service.parentAddChild(family.id, req.body)));
|
|
|
|
api.delete(`${prefix}/me/parent/children/:id`, (req, res) =>
|
|
withFamily(req, res, (family) =>
|
|
service.parentDeleteChild(family.id, +req.params.id)));
|
|
|
|
return { familyRegistry, service };
|
|
}
|
|
|
|
/** @deprecated use attachLearnAssistantPublicRoutes + attachLearnAssistantParentRoutes */
|
|
export function attachLearnAssistantRoutes(api, options = {}) {
|
|
const runtime = attachLearnAssistantPublicRoutes(api, options);
|
|
attachLearnAssistantParentRoutes(api, options);
|
|
return runtime;
|
|
}
|