Public Source Viewer

비나래아카이브 개발자 포털

실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.

Redacted View
src/routes/admin.routes.ts
공개 가능
1 import { Router, Request, Response } from 'express';
2 import fs from 'fs';
3 import path from 'path';
4 import sanitizeHtml from 'sanitize-html';
5 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
6 import { sanitizeOptions } from '../config/sanitize.config';
7 import { requireLogin } from '../middleware/auth.middleware';
8 import { recordSecurityLog } from '../services/security-log.service';
9 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
10
11 const router = Router();
12
13 router.post('/delete', requireLogin, (req: Request, res: Response) => {
14 const postId = req.body.id;
15
16 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
17 if (err) {
18 console.error('Error reading posts file:', err);
19 return res.status(500).send('Internal server error');
20 }
21 let posts: any[];
22 try {
23 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
24 } catch (parseError) {
25 console.error('Error parsing posts file:', parseError);
26 return res.status(500).send('Internal server error');
27 }
28
29 const postIndex = posts.findIndex(post =>
30 String(post.id) === String(postId) &&
31 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
32 );
33 if (postIndex === -1) {
34 recordSecurityLog(req, {
35 type: 'access_denied',
36 target: String(postId || ''),
37 action: '메인 글 삭제 차단',
38 detail: '작성자 또는 관리자가 아닌 사용자의 삭제 시도'
39 });
40 return res.status(403).send('You can only delete your own posts');
41 }
42
43 const deletedPost = posts[postIndex];
44 posts.splice(postIndex, 1);
45
46 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
47 if (err) {
48 console.error('Error writing posts file:', err);
49 return res.status(500).send('Internal server error');
50 }
51 recordSecurityLog(req, {
52 type: 'feature_use',
53 target: String(postId || ''),
54 action: '메인 글 삭제',
55 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
56 });
57 return res.redirect(`/hinana/index`);
58 });
59 });
60 });
61
62 router.post('/batch-delete', requireLogin, (req: Request, res: Response) => {
63 const { postIds } = req.body;
64 const currentUser = req.session.username!;
65
66 if (!postIds || !Array.isArray(postIds) || postIds.length === 0) {
67 return res.status(400).json({ success: false, message: '삭제할 항목이 없습니다.' });
68 }
69
70 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
71 if (err) {
72 console.error('Error reading posts file:', err);
73 return res.status(500).json({ success: false, message: '서버 오류 (파일 읽기)' });
74 }
75
76 let posts: any[];
77 try {
78 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
79 } catch (parseError) {
80 console.error('Error parsing posts file:', parseError);
81 return res.status(500).json({ success: false, message: '서버 오류 (파일 파싱)' });
82 }
83
84 const deletedPosts = posts.filter(post => {
85 const shouldBeDeleted = postIds.includes(String(post.id));
86 const isAdmin = currentUser === '비나래';
87 const isOwner = post.username === currentUser;
88 return shouldBeDeleted && (isAdmin || isOwner);
89 });
90
91 const newPosts = posts.filter(post => {
92 const shouldBeDeleted = postIds.includes(String(post.id));
93
94 if (!shouldBeDeleted) {
95 return true;
96 }
97
98 const isAdmin = currentUser === '비나래';
99 const isOwner = post.username === currentUser;
100
101 if (isAdmin || isOwner) {
102 return false;
103 } else {
104 return true;
105 }
106 });
107
108 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
109 if (err) {
110 console.error('Error writing posts file:', err);
111 return res.status(500).json({ success: false, message: '서버 오류 (파일 쓰기)' });
112 }
113 recordSecurityLog(req, {
114 type: 'feature_use',
115 action: '메인 글 일괄 삭제',
116 detail: `삭제 수: ${deletedPosts.length}, 요청 수: ${postIds.length}`
117 });
118 res.json({ success: true, message: '선택한 항목이 삭제되었습니다.' });
119 });
120 });
121 });
122
123 router.post('/delete-reply', requireLogin, (req: Request, res: Response) => {
124 const { postId, replyId } = req.body;
125
126 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
127 if (err) {
128 console.error('Error reading posts file:', err);
129 return res.status(500).send('Internal server error');
130 }
131
132 let posts: any[];
133 try {
134 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
135 } catch (parseError) {
136 console.error('Error parsing posts file:', parseError);
137 return res.status(500).send('Internal server error');
138 }
139
140 const postIndex = posts.findIndex(post => post.id === postId);
141 if (postIndex === -1) {
142 console.error(`Post not found: postId=${postId}`);
143 return res.status(404).send('Post not found');
144 }
145
146 let replyDeleted = false;
147
148 function deleteReply(replies: any[], parentReplies: any[]): boolean {
149 for (let i = 0; i < replies.length; i++) {
150 const reply = replies[i];
151
152 if (reply.id === replyId) {
153 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
154 recordSecurityLog(req, {
155 type: 'access_denied',
156 target: String(postId || ''),
157 action: '메인 댓글 삭제 차단',
158 detail: `댓글 ID: ${replyId}`
159 });
160 return res.status(403).send('You can only delete your own replies') as any;
161 }
162
163 parentReplies.splice(i, 1);
164 replyDeleted = true;
165 return true;
166 }
167
168 if (reply.replies && reply.replies.length > 0) {
169 if (deleteReply(reply.replies, reply.replies)) {
170 return true;
171 }
172 }
173 }
174 return false;
175 }
176
177 deleteReply(posts[postIndex].replies, posts[postIndex].replies);
178
179 if (!replyDeleted) {
180 console.error(`Reply not found: replyId=${replyId}`);
181 return res.status(404).send('Reply not found');
182 }
183
184 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
185 if (err) {
186 console.error('Error writing posts file:', err);
187 return res.status(500).send('Internal server error');
188 }
189 recordSecurityLog(req, {
190 type: 'feature_use',
191 target: String(postId || ''),
192 action: '메인 댓글 삭제',
193 detail: `댓글 ID: ${replyId}`
194 });
195 res.redirect(`/hinana/index`);
196 });
197 });
198 });
199
200 router.get('/admin/cleanup-xss', requireLogin, (req: Request, res: Response) => {
201 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
202 return res.status(403).send('관리자만 실행할 수 있습니다.');
203 }
204
205 const log: string[] = [];
206
207 function cleanReplies(replies: any[]): void {
208 if (!replies || !Array.isArray(replies)) return;
209 replies.forEach(reply => {
210 reply.content = sanitizeHtml(reply.content, sanitizeOptions);
211 if (reply.replies && reply.replies.length > 0) {
212 cleanReplies(reply.replies);
213 }
214 });
215 }
216
217 try {
218 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
219 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
220 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
221
222 posts.forEach(post => {
223 post.content = sanitizeHtml(post.content, sanitizeOptions);
224 cleanReplies(post.replies);
225 });
226
227 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
228 log.push(`✅ Main Archive (posts.json): ${posts.length}개 게시글 정화 완료`);
229 }
230 } catch (err: any) {
231 log.push(`❌ Main Archive Error: ${err.message}`);
232 }
233
234 try {
235 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
236
237 files.forEach(file => {
238 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
239 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
240 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
241
242 blogPosts.forEach(post => {
243 post.content = sanitizeHtml(post.content, sanitizeOptions);
244 if (post.title) post.title = sanitizeHtml(post.title, { allowedTags: [] });
245 cleanReplies(post.replies);
246 });
247
248 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
249 log.push(`✅ Blog File (${file}): ${blogPosts.length}개 게시글 정화 완료`);
250 });
251
252 } catch (err: any) {
253 log.push(`❌ Blog Error: ${err.message}`);
254 }
255
256 res.send(`
257 <h1>데이터 정화 작업 완료</h1>
258 <pre>${log.join('\n')}</pre>
259 <br>
260 <a href="/hinana/index">메인으로 돌아가기</a>
261 `);
262 });
263
264 export default router;
265