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
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