301 lines
7.2 KiB
JavaScript
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,
|
|
});
|
|
},
|
|
);
|
|
}
|