Files
certremind/src/server/modules/alerts/routes.js
2026-05-23 17:03:05 +09:00

159 lines
4.2 KiB
JavaScript

import { Hono } from 'hono';
import { z } from 'zod';
import { query } from '../../db/pool.js';
import { requireAuth } from '../../middleware/auth.js';
import { badRequest, notFound } from '../../utils/httpErrors.js';
const router = new Hono();
const filterSchema = z.object({
siteId: z.string().uuid().optional(),
alertType: z.string().trim().min(1).max(80).optional(),
from: z.string().datetime({ offset: true }).optional(),
to: z.string().datetime({ offset: true }).optional(),
});
function serializeAlert(row) {
return {
alertId: row.alert_id,
userId: row.user_id,
siteId: row.site_id,
siteAlias: row.site_alias,
siteUrl: row.site_url,
alertType: row.alert_type,
content: row.content,
occurredAt: row.occurred_at,
readAt: row.read_at,
deliveryChannels: row.delivery_channels ?? [],
deliveryResult: row.delivery_result ?? {},
};
}
router.use('*', requireAuth);
router.get('/', async (c) => {
const rawFilters = {
siteId: c.req.query('siteId') || undefined,
alertType: c.req.query('alertType') || undefined,
from: c.req.query('from') || undefined,
to: c.req.query('to') || undefined,
};
const filters = filterSchema.safeParse(rawFilters);
if (!filters.success) {
throw badRequest('絞り込み条件を確認してください', filters.error.flatten());
}
const conditions = ['a.user_id = $1'];
const params = [c.get('user').user_id];
if (filters.data.siteId) {
params.push(filters.data.siteId);
conditions.push(`a.site_id = $${params.length}`);
}
if (filters.data.alertType) {
params.push(filters.data.alertType);
conditions.push(`a.alert_type = $${params.length}`);
}
if (filters.data.from) {
params.push(filters.data.from);
conditions.push(`a.occurred_at >= $${params.length}`);
}
if (filters.data.to) {
params.push(filters.data.to);
conditions.push(`a.occurred_at <= $${params.length}`);
}
const result = await query(
`SELECT a.alert_id,
a.user_id,
a.site_id,
s.alias AS site_alias,
s.url AS site_url,
a.alert_type,
a.content,
a.occurred_at,
a.read_at,
a.delivery_channels,
a.delivery_result
FROM alert_history a
LEFT JOIN sites s ON s.site_id = a.site_id
WHERE ${conditions.join(' AND ')}
ORDER BY a.occurred_at DESC, a.created_at DESC
LIMIT 200`,
params,
);
return c.json({ alerts: result.rows.map(serializeAlert) });
});
router.patch('/:alertId/read', async (c) => {
const alertId = z.string().uuid().safeParse(c.req.param('alertId'));
if (!alertId.success) {
throw badRequest('アラート ID が不正です');
}
const result = await query(
`WITH updated AS (
UPDATE alert_history
SET read_at = COALESCE(read_at, now())
WHERE user_id = $1
AND alert_id = $2
RETURNING alert_id,
user_id,
site_id,
alert_type,
content,
occurred_at,
read_at,
delivery_channels,
delivery_result
)
SELECT u.alert_id,
u.user_id,
u.site_id,
s.alias AS site_alias,
s.url AS site_url,
u.alert_type,
u.content,
u.occurred_at,
u.read_at,
u.delivery_channels,
u.delivery_result
FROM updated u
LEFT JOIN sites s ON s.site_id = u.site_id`,
[c.get('user').user_id, alertId.data],
);
if (!result.rows[0]) {
throw notFound('アラートが見つかりません');
}
return c.json({ alert: serializeAlert(result.rows[0]) });
});
router.delete('/:alertId', async (c) => {
const alertId = z.string().uuid().safeParse(c.req.param('alertId'));
if (!alertId.success) {
throw badRequest('アラート ID が不正です');
}
const result = await query(
`DELETE FROM alert_history
WHERE user_id = $1
AND alert_id = $2
RETURNING alert_id`,
[c.get('user').user_id, alertId.data],
);
if (!result.rows[0]) {
throw notFound('アラートが見つかりません');
}
return c.json({ ok: true });
});
export default router;