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