Public Source Viewer

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

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

Redacted View
src/routes/user.routes.ts
공개 가능
1 import { Router, Request, Response } from 'express';
2 import fs from 'fs';
3 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
4 import { User, Settings } from '../types/models';
5 import { requireLogin } from '../middleware/auth.middleware';
6 import { readSettings } from '../services/settings.service';
7 import { getBookmarks, getTetrisBookmarks, isVerified, getVerifiedUsers, getProfileImage, getDailyPostNotif, toggleDailyPostNotif } from '../services/bookmark.service';
8 import { getSecurityLogs, recordSecurityLog } from '../services/security-log.service';
9 import { analyzeAndSaveBinaraeSignal, getBinaraeSignal } from '../services/binarae-signal.service';
10 import { safeRedirectPath } from '../utils/redirect';
11
12 const router = Router();
13
14 function getSafeReferrerPath(req: Request, fallback: string): string {
15 const referrer = req.get('Referrer');
16 if (!referrer) return fallback;
17
18 try {
19 const url = new URL(referrer);
20 if (url.host !== req.get('host')) return fallback;
21 return safeRedirectPath(`${url.pathname}${url.search}${url.hash}`, fallback);
22 } catch {
23 return safeRedirectPath(referrer, fallback);
24 }
25 }
26
27 function getBinaraeStatus(posts: any[]): string {
28 const allTimestamps: Date[] = [];
29
30 posts.forEach(post => {
31 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
32 allTimestamps.push(new Date(post.timestamp));
33 }
34 if (post.replies) {
35 post.replies.forEach((reply: any) => {
36 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
37 allTimestamps.push(new Date(reply.timestamp));
38 }
39 });
40 }
41 });
42
43 if (allTimestamps.length === 0) {
44 return '현재 수면중 혹은 싱싱미역상태';
45 }
46
47 const latestTimestamp = new Date(Math.max(...allTimestamps.map(d => d.getTime())));
48 const now = new Date();
49 const diffInHours = (now.getTime() - latestTimestamp.getTime()) / (1000 * 60 * 60);
50
51 return diffInHours >= 2 ? '현재 수면중 혹은 싱싱미역상태' : '살아있음!';
52 }
53
54 router.get('/hinana/info', async (req: Request, res: Response) => {
55 const settings = readSettings();
56
57 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
58 if (err) {
59 console.error('Error reading posts file:', err);
60 return res.status(500).send('Internal server error');
61 }
62 let posts: any[];
63 try {
64 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
65 } catch (parseError) {
66 console.error('Error parsing posts file:', parseError);
67 return res.status(500).send('Internal server error');
68 }
69
70 const binaraeStatus = getBinaraeStatus(posts);
71
72 res.render('./hinana/info', {
73 binaraeStatus: binaraeStatus,
74 username: req.session.username || null,
75 theme: req.session.theme || req.cookies.theme || 'light',
76 isSignupEnabled: settings.isSignupEnabled,
77 isAnonymousPostingEnabled: settings.isAnonymousPostingEnabled,
78 isGptEnabled: settings.isGptEnabled,
79 binaraeSignal: getBinaraeSignal(),
80 currentUserProfileImage: req.session.username ? getProfileImage(req.session.username) : null
81 });
82 });
83 });
84
85 router.get('/hinana/userInfo', requireLogin, (req: Request, res: Response) => {
86 const currentUsername = req.session.username!;
87 const theme = req.session.theme || req.cookies.theme || 'light';
88
89 const settings = readSettings();
90
91 if (currentUsername === '비나래') {
92 let users: User[] = [];
93
94 try {
95 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
96 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
97 } catch (err) {
98 console.error('Error reading users file:', err);
99 return res.status(500).send('사용자 목록을 읽을 수 없습니다.');
100 }
101
102 return res.render('./hinana/userInfo', {
103 username: currentUsername,
104 theme,
105 users,
106 bookmarks: getBookmarks(currentUsername),
107 tetrisBookmarks: getTetrisBookmarks(currentUsername),
108 isSignupEnabled: settings.isSignupEnabled,
109 isAnonymousPostingEnabled: settings.isAnonymousPostingEnabled,
110 isGptEnabled: settings.isGptEnabled,
111 verifiedUsers: getVerifiedUsers(),
112 currentUserProfileImage: getProfileImage(currentUsername),
113 dailyPostNotif: getDailyPostNotif(currentUsername),
114 securityLogs: getSecurityLogs(80),
115 binaraeSignal: getBinaraeSignal(),
116 });
117 }
118
119 return res.render('./hinana/userInfo', {
120 username: currentUsername,
121 theme,
122 users: null,
123 bookmarks: getBookmarks(currentUsername),
124 tetrisBookmarks: getTetrisBookmarks(currentUsername),
125 isSignupEnabled: settings.isSignupEnabled,
126 isAnonymousPostingEnabled: settings.isAnonymousPostingEnabled,
127 isGptEnabled: settings.isGptEnabled,
128 verifiedUsers: getVerifiedUsers(),
129 currentUserProfileImage: getProfileImage(currentUsername),
130 dailyPostNotif: getDailyPostNotif(currentUsername),
131 securityLogs: [],
132 binaraeSignal: getBinaraeSignal(),
133 });
134 });
135
136 router.post('/hinana/admin/binarae-signal', requireLogin, async (req: Request, res: Response) => {
137 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
138 recordSecurityLog(req, {
139 type: 'access_denied',
140 action: '오늘의 신호 변경 차단',
141 detail: '관리자 권한이 없는 사용자의 오늘의 신호 변경 시도'
142 });
143 return res.status(403).send('권한이 없습니다.');
144 }
145
146 const signalText = String(req.body.signal_text || '').trim().slice(0, 240);
147 const signal = await analyzeAndSaveBinaraeSignal(signalText);
148
149 recordSecurityLog(req, {
150 type: 'admin_action',
151 action: '오늘의 신호 변경',
152 detail: `${signal.statusText} / ${signal.keywords.join(', ')}`
153 });
154
155 res.redirect('/hinana/userInfo#binarae-signal');
156 });
157
158 router.post('/hinana/toggle-anonymous-posting', requireLogin, (req: Request, res: Response) => {
159 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
160 recordSecurityLog(req, {
161 type: 'access_denied',
162 action: '익명 글 작성 설정 변경 차단',
163 detail: '관리자 권한이 없는 사용자의 설정 변경 시도'
164 });
165 return res.status(403).send('권한이 없습니다.');
166 }
167
168 let settings = readSettings();
169 settings.isAnonymousPostingEnabled = !settings.isAnonymousPostingEnabled;
170
171 try {
172 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
173 } catch (err) {
174 console.error("Error writing settings file:", err);
175 return res.status(500).send("설정 저장 실패");
176 }
177
178 recordSecurityLog(req, {
179 type: 'admin_action',
180 action: '익명 글 작성 설정 변경',
181 detail: settings.isAnonymousPostingEnabled ? '활성화' : '비활성화'
182 });
183
184 res.redirect(req.get('Referrer') || '/');
185 });
186
187 router.post('/hinana/toggle-signup', requireLogin, (req: Request, res: Response) => {
188 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
189 recordSecurityLog(req, {
190 type: 'access_denied',
191 action: '회원가입 설정 변경 차단',
192 detail: '관리자 권한이 없는 사용자의 설정 변경 시도'
193 });
194 return res.status(403).send('권한이 없습니다.');
195 }
196
197 let settings = readSettings();
198 settings.isSignupEnabled = !settings.isSignupEnabled;
199
200 try {
201 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
202 } catch (err) {
203 console.error("Error writing settings file:", err);
204 return res.status(500).send("설정 저장 실패");
205 }
206
207 recordSecurityLog(req, {
208 type: 'admin_action',
209 action: '회원가입 설정 변경',
210 detail: settings.isSignupEnabled ? '활성화' : '비활성화'
211 });
212
213 res.redirect(req.get('Referrer') || '/');
214 });
215
216 router.post('/hinana/toggle-gpt', requireLogin, (req: Request, res: Response) => {
217 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
218 recordSecurityLog(req, {
219 type: 'access_denied',
220 action: 'GPT 채팅 설정 변경 차단',
221 detail: '관리자 권한이 없는 사용자의 설정 변경 시도'
222 });
223 return res.status(403).send('권한이 없습니다.');
224 }
225
226 let settings = readSettings();
227 settings.isGptEnabled = !(settings.isGptEnabled !== false);
228
229 try {
230 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
231 } catch (err) {
232 console.error("Error writing settings:", err);
233 return res.status(500).send("설정 저장 실패");
234 }
235 recordSecurityLog(req, {
236 type: 'admin_action',
237 action: 'GPT 채팅 설정 변경',
238 detail: settings.isGptEnabled ? '활성화' : '비활성화'
239 });
240 res.redirect(req.get('Referrer') || '/');
241 });
242
243 router.post('/hinana/delete-user', requireLogin, (req: Request, res: Response) => {
244 const adminUsername = req.session.username;
245 const usernameToDelete = req.body.usernameToDelete;
246
247 if (adminUsername !== '비나래') {
248 recordSecurityLog(req, {
249 type: 'access_denied',
250 action: '사용자 삭제 차단',
251 target: String(usernameToDelete || ''),
252 detail: '관리자 권한이 없는 사용자의 사용자 삭제 시도'
253 });
254 return res.status(403).send('권한이 없습니다.');
255 }
256
257 if (usernameToDelete === '비나래') {
258 return res.status(400).send('관리자 계정은 삭제할 수 없습니다.');
259 }
260
261 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
262 if (err) {
263 console.error('Error reading users file:', err);
264 return res.status(500).send('Internal server error');
265 }
266
267 let users: User[];
268 try {
269 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
270 } catch (parseError) {
271 console.error('Error parsing users file:', parseError);
272 return res.status(500).send('Internal server error');
273 }
274
275 const updatedUsers = users.filter(user => user.username !== usernameToDelete);
276
277 if (users.length === updatedUsers.length) {
278 return res.status(404).send('삭제할 사용자를 찾을 수 없습니다.');
279 }
280
281 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
282 if (writeErr) {
283 console.error('Error writing users file:', writeErr);
284 return res.status(500).send('Internal server error');
285 }
286
287 recordSecurityLog(req, {
288 type: 'admin_action',
289 action: '사용자 삭제',
290 target: String(usernameToDelete || '')
291 });
292
293 res.redirect('/hinana/userInfo');
294 });
295 });
296 });
297
298 router.post('/hinana/toggle-daily-post-notif', (req: Request, res: Response) => {
299 if (!req.session.username) return res.status(401).send('로그인이 필요합니다.');
300 toggleDailyPostNotif(req.session.username);
301 recordSecurityLog(req, {
302 type: 'feature_use',
303 action: '오늘의 글 알림 설정 변경'
304 });
305 res.redirect(req.get('Referrer') || '/hinana/userInfo');
306 });
307
308 router.post('/toggle-theme', (req: Request, res: Response) => {
309 const username = req.session.username;
310 const cookieTheme = req.cookies.theme || 'light';
311 const sessionTheme = req.session.theme || cookieTheme;
312 const newTheme = sessionTheme === 'dark' ? 'light' : 'dark';
313 const redirectTo = safeRedirectPath(req.body?.redirect, getSafeReferrerPath(req, '/hinana/index'));
314
315 req.session.theme = newTheme;
316 res.cookie('theme', newTheme, {
317 maxAge: 365 * 24 * 60 * 60 * 1000,
318 sameSite: 'lax',
319 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
320 });
321
322 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
323 try {
324 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
325 const user = users.find(u => u.username === username);
326 if (user) {
327 user.theme = newTheme;
328 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
329 }
330 } catch (error) {
331 console.error('Theme user preference save error:', error);
332 }
333 }
334
335 req.session.save((err) => {
336 if (err) {
337 console.error('Session save error:', err);
338 }
339 res.redirect(redirectTo);
340 });
341 });
342
343 export default router;
344