Files
memind/server/portal-notification-routes.mjs

301 lines
7.2 KiB
JavaScript

export function attachPortalNotificationRoutes({
app,
userAuthReady = Promise.resolve(),
getUserAuth = () => null,
getScheduleService = () => null,
userToken,
logger = console,
setIntervalFn = setInterval,
clearIntervalFn = clearInterval,
now = Date.now,
} = {}) {
if (
!app ||
typeof userToken !== 'function' ||
typeof setIntervalFn !== 'function' ||
typeof clearIntervalFn !== 'function' ||
typeof now !== 'function'
) {
throw new Error(
'attachPortalNotificationRoutes requires route dependencies',
);
}
app.get('/auth/notifications', async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
const scheduleService = getScheduleService();
if (!userAuth || !scheduleService) {
return res.status(503).json({
message: '通知服务未启用',
});
}
const me = await userAuth.getMe(userToken(req));
if (!me) {
return res.status(401).json({
message: '未登录',
});
}
const rawStatus =
typeof req.query?.status === 'string'
? req.query.status
: 'unread';
const status = [
'all',
'unread',
'read',
].includes(rawStatus)
? rawStatus
: 'all';
const limit = Math.min(
Math.max(Number(req.query?.limit) || 20, 1),
100,
);
try {
const notifications =
await scheduleService.listUserNotifications({
userId: me.id,
status,
limit,
});
res.json({ notifications });
} catch (error) {
logger.warn(
'List user notifications failed:',
error instanceof Error
? error.message
: error,
);
res.status(500).json({
message: '通知列表加载失败',
});
}
});
app.get(
'/auth/notifications/events',
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
const scheduleService =
getScheduleService();
if (!userAuth || !scheduleService) {
return res.status(503).json({
message: '通知服务未启用',
});
}
const me = await userAuth.getMe(
userToken(req),
);
if (!me) {
return res.status(401).json({
message: '未登录',
});
}
res.status(200);
res.setHeader(
'Content-Type',
'text/event-stream; charset=utf-8',
);
res.setHeader(
'Cache-Control',
'no-cache, no-transform',
);
res.setHeader('Connection', 'keep-alive');
res.flushHeaders?.();
let closed = false;
let lastNotificationId = null;
const sendEvent = (event, data) => {
if (closed || res.destroyed) return;
res.write(`event: ${event}\n`);
res.write(
`data: ${JSON.stringify(data)}\n\n`,
);
};
const checkUnread = async () => {
if (closed) return;
try {
const notifications =
await scheduleService.listUserNotifications({
userId: me.id,
status: 'unread',
limit: 1,
});
const latest = notifications[0] ?? null;
const nextId = latest?.id ?? null;
if (
nextId &&
nextId !== lastNotificationId
) {
lastNotificationId = nextId;
sendEvent('notification', {
notification: latest,
});
} else if (!nextId) {
lastNotificationId = null;
}
} catch {
sendEvent('sync', {
reason: 'check_failed',
});
}
};
sendEvent('ready', { ok: true });
await checkUnread();
const checkTimer = setIntervalFn(() => {
void checkUnread();
}, 2500);
const keepaliveTimer = setIntervalFn(() => {
sendEvent('ping', { at: now() });
}, 25000);
req.on('close', () => {
closed = true;
clearIntervalFn(checkTimer);
clearIntervalFn(keepaliveTimer);
});
},
);
app.post(
'/auth/notifications/:id/read',
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
const scheduleService =
getScheduleService();
if (!userAuth || !scheduleService) {
return res.status(503).json({
message: '通知服务未启用',
});
}
const me = await userAuth.getMe(
userToken(req),
);
if (!me) {
return res.status(401).json({
message: '未登录',
});
}
const ok =
await scheduleService.markUserNotificationRead({
userId: me.id,
notificationId: req.params.id,
});
if (!ok) {
return res.status(404).json({
message: '通知不存在或已读',
});
}
res.json({ ok: true });
},
);
app.post(
'/auth/notifications/read-all',
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
const scheduleService =
getScheduleService();
if (!userAuth || !scheduleService) {
return res.status(503).json({
message: '通知服务未启用',
});
}
const me = await userAuth.getMe(
userToken(req),
);
if (!me) {
return res.status(401).json({
message: '未登录',
});
}
const updated =
await scheduleService.markAllUserNotificationsRead(
{ userId: me.id },
);
res.json({
ok: true,
updated,
});
},
);
app.delete(
'/auth/notifications/:id',
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
const scheduleService =
getScheduleService();
if (!userAuth || !scheduleService) {
return res.status(503).json({
message: '通知服务未启用',
});
}
const me = await userAuth.getMe(
userToken(req),
);
if (!me) {
return res.status(401).json({
message: '未登录',
});
}
const ok =
await scheduleService.deleteUserNotification({
userId: me.id,
notificationId: req.params.id,
});
if (!ok) {
return res.status(404).json({
message: '通知不存在',
});
}
res.status(204).end();
},
);
app.delete(
'/auth/notifications',
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
const scheduleService =
getScheduleService();
if (!userAuth || !scheduleService) {
return res.status(503).json({
message: '通知服务未启用',
});
}
const me = await userAuth.getMe(
userToken(req),
);
if (!me) {
return res.status(401).json({
message: '未登录',
});
}
const status =
typeof req.query?.status === 'string'
? req.query.status
: 'all';
const deleted =
await scheduleService.clearUserNotifications({
userId: me.id,
status,
});
res.json({
ok: true,
deleted,
});
},
);
}