Initial import

This commit is contained in:
how2ice
2026-04-21 03:33:23 +09:00
commit 9e4b70f1f1
495 changed files with 94680 additions and 0 deletions

View File

@@ -0,0 +1,172 @@
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { hasErrorLogViewAccessToken } from '../services/error-log-service.js';
import {
BoardPostAutomationLockedError,
boardPostPayloadSchema,
createBoardPost,
deleteBoardPost,
ensureBoardPostsTable,
getBoardPost,
listBoardPosts,
receiveBoardPostAutomation,
updateBoardPost,
} from '../services/board-service.js';
export async function registerBoardRoutes(app: FastifyInstance) {
function isLoopbackAddress(value: string | null | undefined) {
const normalizedValue = String(value ?? '').trim();
return normalizedValue === '127.0.0.1' || normalizedValue === '::1' || normalizedValue === '::ffff:127.0.0.1';
}
function hasBoardAutomationAccess(request: { headers: Record<string, unknown>; ip?: string; socket?: { remoteAddress?: string | null }; raw?: { socket?: { remoteAddress?: string | null } } }) {
if (hasErrorLogViewAccessToken(request.headers['x-access-token'] as string | string[] | undefined)) {
return true;
}
return isLoopbackAddress(request.ip) || isLoopbackAddress(request.raw?.socket?.remoteAddress) || isLoopbackAddress(request.socket?.remoteAddress);
}
function requireBoardAutomationAccess(
request: { headers: Record<string, unknown>; ip?: string; socket?: { remoteAddress?: string | null }; raw?: { socket?: { remoteAddress?: string | null } } },
reply: Parameters<FastifyInstance['get']>[1] extends (request: any, reply: infer T) => any ? T : any,
) {
if (hasBoardAutomationAccess(request)) {
return true;
}
void reply.code(403).send({
message: '권한 토큰이 등록된 사용자만 자동화 접수를 할 수 있습니다.',
});
return false;
}
async function respondWithBoardSetup() {
await ensureBoardPostsTable();
return {
ok: true,
table: 'board_posts',
};
}
async function respondWithBoardPosts() {
const items = await listBoardPosts();
return {
ok: true,
items,
};
}
app.get('/api/board/setup', async () => respondWithBoardSetup());
app.post('/api/board/setup', async () => {
return respondWithBoardSetup();
});
app.get('/api/board/posts', async () => respondWithBoardPosts());
app.get('/api/board/items', async () => respondWithBoardPosts());
app.get('/api/board/posts/:id', async (request, reply) => {
const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
const item = await getBoardPost(id);
if (!item) {
return reply.code(404).send({
message: '게시글을 찾을 수 없습니다.',
});
}
return {
ok: true,
item,
};
});
app.post('/api/board/posts', async (request) => {
const item = await createBoardPost(boardPostPayloadSchema.parse(request.body ?? {}));
return {
ok: true,
item,
};
});
app.post('/api/board/posts/:id/actions/automation-receive', async (request, reply) => {
if (!requireBoardAutomationAccess(request, reply)) {
return;
}
const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
const result = await receiveBoardPostAutomation(id);
if (!result) {
return reply.code(404).send({
message: '자동화 접수할 게시글을 찾을 수 없습니다.',
});
}
return {
ok: true,
item: result.item,
planItemId: result.planItemId,
alreadyReceived: result.alreadyReceived,
};
});
app.patch('/api/board/posts/:id', async (request, reply) => {
const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
let item;
try {
item = await updateBoardPost(id, boardPostPayloadSchema.parse(request.body ?? {}));
} catch (error) {
if (error instanceof BoardPostAutomationLockedError) {
return reply.code(409).send({
message: error.message,
});
}
throw error;
}
if (!item) {
return reply.code(404).send({
message: '수정할 게시글을 찾을 수 없습니다.',
});
}
return {
ok: true,
item,
};
});
app.delete('/api/board/posts/:id', async (request, reply) => {
const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
let deleted;
try {
deleted = await deleteBoardPost(id);
} catch (error) {
if (error instanceof BoardPostAutomationLockedError) {
return reply.code(409).send({
message: error.message,
});
}
throw error;
}
if (!deleted) {
return reply.code(404).send({
message: '삭제할 게시글을 찾을 수 없습니다.',
});
}
return {
ok: true,
id,
};
});
}