Public Source Viewer

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

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

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