Skip to content

Commit 0da3288

Browse files
junzero741claude
andcommitted
feat(backend): add report and admin routers
- POST /posts/:slug/report - GET /admin/reports, DELETE /admin/posts/:slug, POST /admin/reports/:id/dismiss - 관리자 API는 Bearer 토큰(ADMIN_SECRET) 인증 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent eedb32d commit 0da3288

3 files changed

Lines changed: 94 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Router, Request, Response, NextFunction } from 'express';
2+
import { ROUTE_PATTERNS } from '@private-board/shared';
3+
import { AppError } from '../lib/errors';
4+
import { getReports, deletePost, dismissReport } from './admin.service';
5+
6+
const router = Router();
7+
8+
function requireAdminSecret(req: Request, _res: Response, next: NextFunction) {
9+
const secret = process.env.ADMIN_SECRET;
10+
if (!secret) {
11+
return next(new AppError(503, 'Admin not configured', 'ADMIN_NOT_CONFIGURED'));
12+
}
13+
const auth = req.headers.authorization;
14+
if (!auth || auth !== `Bearer ${secret}`) {
15+
return next(new AppError(401, 'Unauthorized', 'UNAUTHORIZED'));
16+
}
17+
next();
18+
}
19+
20+
router.use(requireAdminSecret);
21+
22+
router.get(ROUTE_PATTERNS.admin.reports, async (
23+
_req: Request,
24+
res: Response,
25+
next: NextFunction
26+
) => {
27+
try {
28+
const reports = await getReports();
29+
res.json(reports);
30+
} catch (err) {
31+
next(err);
32+
}
33+
});
34+
35+
router.delete(ROUTE_PATTERNS.admin.deletePost, async (
36+
req: Request<{ slug: string }>,
37+
res: Response,
38+
next: NextFunction
39+
) => {
40+
try {
41+
await deletePost(req.params.slug);
42+
res.status(204).end();
43+
} catch (err) {
44+
next(err);
45+
}
46+
});
47+
48+
router.post(ROUTE_PATTERNS.admin.dismissReport, async (
49+
req: Request<{ id: string }>,
50+
res: Response,
51+
next: NextFunction
52+
) => {
53+
try {
54+
await dismissReport(req.params.id);
55+
res.status(204).end();
56+
} catch (err) {
57+
next(err);
58+
}
59+
});
60+
61+
export default router;

apps/backend/src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { AppError } from './lib/errors';
77
import { cleanupExpiredPosts } from './lib/cleanup';
88
import postsRouter from './posts/posts.router';
99
import uploadsRouter from './uploads/uploads.router';
10+
import reportsRouter from './reports/reports.router';
11+
import adminRouter from './admin/admin.router';
1012

1113
const host = process.env.HOST ?? '0.0.0.0';
1214
const port = process.env.PORT ? Number(process.env.PORT) : 4000;
@@ -43,6 +45,8 @@ app.use('/uploads', express.static(uploadsDir));
4345

4446
app.use('/posts', postsRouter);
4547
app.use('/uploads', uploadsRouter);
48+
app.use('/posts', reportsRouter);
49+
app.use('/admin', adminRouter);
4650

4751
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
4852
if (err instanceof AppError) {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Router, Request, Response, NextFunction } from 'express';
2+
import { ROUTE_PATTERNS, CreateReportRequest } from '@private-board/shared';
3+
import { AppError } from '../lib/errors';
4+
import { createReport } from './reports.service';
5+
6+
const router = Router();
7+
8+
router.post(ROUTE_PATTERNS.posts.report, async (
9+
req: Request<{ slug: string }, void, Omit<CreateReportRequest, 'slug'>>,
10+
res: Response,
11+
next: NextFunction
12+
) => {
13+
try {
14+
const { slug } = req.params;
15+
const { reason, description } = req.body;
16+
const ip = req.ip ?? '0.0.0.0';
17+
18+
if (!reason) {
19+
throw new AppError(400, 'reason is required', 'VALIDATION_ERROR');
20+
}
21+
22+
await createReport(slug, reason, description, ip);
23+
res.status(201).end();
24+
} catch (err) {
25+
next(err);
26+
}
27+
});
28+
29+
export default router;

0 commit comments

Comments
 (0)