Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
src/routes/persona.routes.ts
공개 가능
1
import { Router, Request, Response } from 'express';
2
import sanitizeHtml from 'sanitize-html';
3
import { getPersonaByDiscordId, getPersonaByUsername, savePersona } from '../services/persona.service';
4
import { spendBookmarks, getBookmarks } from '../services/bookmark.service';
5
6
const router = Router();
7
8
const PERSONA_COST = 30;
9
const PERSONA_UPDATE_COST = 20;
10
11
// 페르소나 설정 페이지
12
router.get('/hinana/persona', (req: Request, res: Response) => {
13
if (!req.session.username) return res.redirect(`/login?redirect=${encodeURIComponent(req.originalUrl)}`);
14
const username = req.session.username;
15
16
// discord_id는 Discord 봇 링크에서 최초 1회만 쿼리로 받아 세션에 저장
17
// 이후 URL 조작으로 다른 discord_id를 주입하는 것을 방지
18
if (req.query.discord_id) {
19
req.session.personaDiscordId = req.query.discord_id as string;
20
req.session.personaDiscordName = req.query.discord_name as string || '';
21
}
22
23
const discordId: string = req.session.personaDiscordId || '';
24
const discordName: string = req.session.personaDiscordName || '';
25
26
const existing = discordId
27
? getPersonaByDiscordId(discordId)
28
: getPersonaByUsername(username);
29
30
// 기존 페르소나가 있는 경우, 소유자 검증: 세션 유저의 페르소나인지 확인
31
if (existing && existing.hinanaUsername !== username) {
32
return res.status(403).send('본인의 페르소나만 수정할 수 있습니다.');
33
}
34
35
res.render('./hinana/persona', {
36
username,
37
theme: req.session.theme || req.cookies.theme || 'light',
38
discordId,
39
discordName: existing ? existing.discordUsername : discordName,
40
existingPersona: existing,
41
bookmarks: getBookmarks(username),
42
personaCost: PERSONA_COST,
43
personaUpdateCost: PERSONA_UPDATE_COST,
44
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
45
});
46
});
47
48
// 페르소나 저장
49
router.post('/hinana/persona/save', (req: Request, res: Response) => {
50
if (!req.session.username) return res.redirect('/login');
51
const username = req.session.username;
52
53
// discord_id는 반드시 세션에서만 읽음 — body 주입 불가
54
const discord_id: string = req.session.personaDiscordId || '';
55
const discord_name: string = req.session.personaDiscordName || '';
56
const { persona_name, persona_text } = req.body;
57
58
if (!discord_id || !persona_text || !persona_text.trim()) {
59
return res.redirect('/hinana/persona?error=' + encodeURIComponent('디스코드 계정 연동 및 페르소나 내용이 필요합니다.'));
60
}
61
62
// \r\n 정규화 후 길이 체크
63
const normalizedText = persona_text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
64
if (normalizedText.trim().length > 2000) {
65
return res.redirect('/hinana/persona?error=' + encodeURIComponent('페르소나는 2000자 이내여야 합니다.'));
66
}
67
68
// 기존 페르소나가 있는 경우 소유자 검증
69
const existing = getPersonaByDiscordId(discord_id);
70
if (existing && existing.hinanaUsername !== username) {
71
return res.redirect('/hinana/persona?error=' + encodeURIComponent('본인의 페르소나만 수정할 수 있습니다.'));
72
}
73
74
const trimmedName = sanitizeHtml((persona_name || '').trim().slice(0, 50), { allowedTags: [] });
75
76
const isNew = !existing;
77
const cost = isNew ? PERSONA_COST : PERSONA_UPDATE_COST;
78
79
const result = spendBookmarks(username, cost);
80
if (!result.success) {
81
return res.redirect('/hinana/persona?error=' + encodeURIComponent(`책갈피가 부족합니다. (필요: ${cost}개, 보유: ${getBookmarks(username)}개)`));
82
}
83
84
savePersona({
85
hinanaUsername: username,
86
discordUserId: discord_id,
87
discordUsername: discord_name || 'Unknown',
88
personaName: trimmedName,
89
persona: normalizedText.trim()
90
});
91
92
res.redirect(`/hinana/persona?saved=1`);
93
});
94
95
export default router;
96