First commit

This commit is contained in:
CyberRex
2026-05-23 17:03:05 +09:00
commit 40e7953ee5
52 changed files with 13004 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
import { Hono } from 'hono';
import { z } from 'zod';
import { env } from '../../config/env.js';
import { query } from '../../db/pool.js';
import { requireAuth } from '../../middleware/auth.js';
import { badRequest, notFound } from '../../utils/httpErrors.js';
import { normalizeHttpsUrl } from '../../utils/urlPolicy.js';
const router = new Hono();
const webhookSchema = z.object({
alias: z.string().trim().min(1).max(120),
url: z.string().trim().min(1).max(2048),
});
const pushSubscriptionSchema = z.object({
endpoint: z.string().trim().url().max(2048),
keys: z.object({
p256dh: z.string().trim().min(1).max(512),
auth: z.string().trim().min(1).max(512),
}),
});
function serializeWebhook(row) {
return {
notificationMethodId: row.notification_method_id,
alias: row.alias,
url: row.url,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
function serializePushSubscription(row) {
return {
notificationMethodId: row.notification_method_id,
endpoint: row.push_endpoint,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
router.use('*', requireAuth);
router.get('/', async (c) => {
const user = c.get('user');
const result = await query(
`SELECT notification_method_id,
notification_type,
alias,
url,
push_endpoint,
created_at,
updated_at
FROM notification_methods
WHERE user_id = $1
ORDER BY created_at DESC`,
[user.user_id],
);
return c.json({
webhooks: result.rows
.filter((row) => row.notification_type === 'webhook')
.map(serializeWebhook),
pushSubscriptions: result.rows
.filter((row) => row.notification_type === 'push')
.map(serializePushSubscription),
vapidPublicKey: env.vapidPublicKey,
});
});
router.post('/webhooks', async (c) => {
const body = webhookSchema.safeParse(await c.req.json().catch(() => null));
if (!body.success) {
throw badRequest('入力内容を確認してください', body.error.flatten());
}
let normalizedUrl;
try {
normalizedUrl = normalizeHttpsUrl(body.data.url);
} catch (error) {
throw badRequest(error.message);
}
const result = await query(
`INSERT INTO notification_methods (user_id, notification_type, alias, url)
VALUES ($1, 'webhook', $2, $3)
RETURNING notification_method_id, alias, url, created_at, updated_at`,
[c.get('user').user_id, body.data.alias, normalizedUrl],
);
return c.json({ webhook: serializeWebhook(result.rows[0]) }, 201);
});
router.patch('/webhooks/:methodId', async (c) => {
const methodId = z.string().uuid().safeParse(c.req.param('methodId'));
if (!methodId.success) {
throw badRequest('通知方法 ID が不正です');
}
const body = webhookSchema.safeParse(await c.req.json().catch(() => null));
if (!body.success) {
throw badRequest('入力内容を確認してください', body.error.flatten());
}
let normalizedUrl;
try {
normalizedUrl = normalizeHttpsUrl(body.data.url);
} catch (error) {
throw badRequest(error.message);
}
const result = await query(
`UPDATE notification_methods
SET alias = $3,
url = $4
WHERE user_id = $1
AND notification_method_id = $2
AND notification_type = 'webhook'
RETURNING notification_method_id, alias, url, created_at, updated_at`,
[c.get('user').user_id, methodId.data, body.data.alias, normalizedUrl],
);
if (!result.rows[0]) {
throw notFound('Webhook が見つかりません');
}
return c.json({ webhook: serializeWebhook(result.rows[0]) });
});
router.delete('/webhooks/:methodId', async (c) => {
const methodId = z.string().uuid().safeParse(c.req.param('methodId'));
if (!methodId.success) {
throw badRequest('通知方法 ID が不正です');
}
const result = await query(
`DELETE FROM notification_methods
WHERE user_id = $1
AND notification_method_id = $2
AND notification_type = 'webhook'
RETURNING notification_method_id`,
[c.get('user').user_id, methodId.data],
);
if (!result.rows[0]) {
throw notFound('Webhook が見つかりません');
}
return c.json({ ok: true });
});
router.post('/push-subscriptions', async (c) => {
const body = pushSubscriptionSchema.safeParse(await c.req.json().catch(() => null));
if (!body.success) {
throw badRequest('購読情報を確認してください', body.error.flatten());
}
const endpoint = new URL(body.data.endpoint);
if (endpoint.protocol !== 'https:') {
throw badRequest('Push endpoint は HTTPS である必要があります');
}
const user = c.get('user');
await query(
`DELETE FROM notification_methods
WHERE user_id = $1
AND notification_type = 'push'
AND push_endpoint = $2`,
[user.user_id, body.data.endpoint],
);
const result = await query(
`INSERT INTO notification_methods
(user_id, notification_type, alias, push_endpoint, push_p256dh, push_auth)
VALUES ($1, 'push', 'Browser Push', $2, $3, $4)
RETURNING notification_method_id, push_endpoint, created_at, updated_at`,
[user.user_id, body.data.endpoint, body.data.keys.p256dh, body.data.keys.auth],
);
return c.json({ pushSubscription: serializePushSubscription(result.rows[0]) }, 201);
});
export default router;