Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
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