173 lines
4.7 KiB
TypeScript
173 lines
4.7 KiB
TypeScript
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,
|
|
planItemIds: result.planItemIds,
|
|
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,
|
|
};
|
|
});
|
|
}
|