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