/** npm install hono jsonwebtoken bcrypt npm install -D typescript ts-node @types/node @types/jsonwebtoken @types/bcrypt */ //@ts-ignore import bcrypt from "bcrypt"; //@ts-ignore import jwt from "jsonwebtoken"; //@ts-ignore import crypto from "crypto"; import { v4 as uuidv4 } from "uuid"; // πŸ”‘ ν‚€ 길이λ₯Ό AES-256-CBC μš”κ΅¬μ‚¬ν•­μΈ 32λ°”μ΄νŠΈλ‘œ λ§žμΆ”λŠ” μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜ // 길이가 λΆ€μ‘±ν•˜λ©΄ 0x00으둜 νŒ¨λ”©ν•˜κ³ , μ΄ˆκ³Όν•˜λ©΄ μž˜λΌλƒ…λ‹ˆλ‹€. (λ³΄μ•ˆ κ²½κ³ ) const getEncryptionKeyBuffer = (): Buffer => { const KEY_BYTE_LENGTH = 32; let keyBuffer = Buffer.from(ENCRYPTION_KEY, "utf8"); if (keyBuffer.length === KEY_BYTE_LENGTH) { return keyBuffer; } if (keyBuffer.length > KEY_BYTE_LENGTH) { // 32λ°”μ΄νŠΈ 초과 μ‹œ, μ•ž λΆ€λΆ„λ§Œ μ‚¬μš© (μž˜λΌλƒ„) return keyBuffer.subarray(0, KEY_BYTE_LENGTH); } else { // 32λ°”μ΄νŠΈ 미달 μ‹œ, 0으둜 μ±„μ›Œμ„œ (νŒ¨λ”©) 32λ°”μ΄νŠΈλ₯Ό λ§Œλ“­λ‹ˆλ‹€. const padding = Buffer.alloc(KEY_BYTE_LENGTH - keyBuffer.length, 0); return Buffer.concat([keyBuffer, padding]); } }; const JWT_SECRET = String(process.env.JWT_SECRET); const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || "your_32_byte_encryption_key_123456"; // 32 bytes const ENCRYPTION_KEY_BUFFER = getEncryptionKeyBuffer(); const IV_LENGTH = 16; // AES block size // 단방ν–₯ μ•”ν˜Έν™”: λΉ„λ°€λ²ˆν˜Έ ν•΄μ‹œ 생성 export const hashPassword = async (password: string): Promise => { const saltRounds = 10; return await bcrypt.hash(password, saltRounds); }; // 단방ν–₯ μ•”ν˜Έν™”: λΉ„λ°€λ²ˆν˜Έ 검증 export const comparePassword = async ( password: string, hash: string ): Promise => { return await bcrypt.compare(password, hash); }; // μ–‘λ°©ν–₯ μ•”ν˜Έν™”: 데이터 μ•”ν˜Έν™” export const encryptData = (data: string): string => { const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv( "aes-256-cbc", ENCRYPTION_KEY_BUFFER, iv ); let encrypted = cipher.update(data); encrypted = Buffer.concat([encrypted, cipher.final()]); return iv.toString("hex") + ":" + encrypted.toString("hex"); }; // μ–‘λ°©ν–₯ μ•”ν˜Έν™”: 데이터 λ³΅ν˜Έν™” export const decryptData = (encryptedData: string): string => { const parts = encryptedData.split(":"); const iv = Buffer.from(parts[0], "hex"); const encryptedText = Buffer.from(parts[1], "hex"); const decipher = crypto.createDecipheriv( "aes-256-cbc", ENCRYPTION_KEY_BUFFER, iv ); let decrypted = decipher.update(encryptedText); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString(); }; // JWT 생성 export const generateToken = ( payload: any, expiresIn: string = "1h" ): string => { //@ts-ignore return jwt.sign(payload, JWT_SECRET, { expiresIn }); }; /** JWT 검증. return := payload */ export const verifyToken = (token: string): any => { try { return jwt.verify(token, JWT_SECRET); } catch (error) { return null; } }; // JWT 해독 (검증 없이 νŽ˜μ΄λ‘œλ“œλ§Œ μΆ”μΆœ) export const decodeToken = (token: string): object | null => { try { const payload = token.split(".")[1]; const decoded = Buffer.from(payload, "base64").toString("utf-8"); return JSON.parse(decoded); } catch (error) { return null; } }; /** * ν˜„μž¬ μ‹œκ°„(λ°€λ¦¬μ΄ˆ)κ³Ό UUIDλ₯Ό μ‘°ν•©ν•˜μ—¬ 파일 μ΄λ¦„μœΌλ‘œ μ•ˆμ „ν•˜κ²Œ μ‚¬μš©ν•  수 μžˆλŠ” λ¬Έμžμ—΄μ„ μƒμ„±ν•©λ‹ˆλ‹€. * μƒμ„±λœ λ¬Έμžμ—΄μ˜ κΈΈμ΄λŠ” 255자 λ―Έλ§Œμž…λ‹ˆλ‹€. * (UUID: 36자, λ°€λ¦¬μ΄ˆ: μ•½ 13자, κ΅¬λΆ„μž: 1자 = μ΅œλŒ€ μ•½ 50자) * * @returns {string} μ‘°ν•©λœ 파일 이름 λ¬Έμžμ—΄ (예: "1730635200000-a1b2c3d4-e5f6-4000-8000-000000000000") */ export function createUniqueFileName(): string { // 1. ν˜„μž¬ μ‹œκ°„μ„ λ°€λ¦¬μ΄ˆ λ‹¨μœ„λ‘œ κ°€μ Έμ˜΅λ‹ˆλ‹€. const timestamp = Date.now().toString(); // 2. UUID v4λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. (예: "a1b2c3d4-e5f6-4000-8000-000000000000") // 이 λ¬Έμžμ—΄μ€ 파일 μ΄λ¦„μœΌλ‘œ μ•ˆμ „ν•˜κ²Œ μ‚¬μš©λ  수 μžˆλŠ” ν•˜μ΄ν”ˆμ„ ν¬ν•¨ν•©λ‹ˆλ‹€. const uniqueId = uuidv4(); // 3. 두 값을 ν•˜μ΄ν”ˆ(-)으둜 μ—°κ²°ν•©λ‹ˆλ‹€. const uniqueFileName = `${timestamp}-${uniqueId}`; // λ¬Έμžμ—΄ 길이 확인 (255자 λ―Έλ§Œμ€ ν™•μ‹€νžˆ λ§Œμ‘±ν•©λ‹ˆλ‹€) // console.log(`μƒμ„±λœ 파일 이름: ${uniqueFileName}, 길이: ${uniqueFileName.length}`); return uniqueFileName; } /** * 이미지 κ²½λ‘œκ°€ ν΄λ”λ‘œ 되있으면, 이걸 μ„œλ²„μ—μ„œ 직접 슀트리밍 ν•˜λŠ” μ£Όμ†Œλ‘œ λ°”κΏ‰λ‹ˆλ‹€. * ν•΄λ‹Ή ν”„λ‘œμ νŠΈ κ²Œμ‹œνŒ μ „μš©μœΌλ‘œ λ§Œλ“€μ–΄μ‘ŒμŠ΅λ‹ˆλ‹€. λ²”μš© μ»΄ν¬λ„ŒνŠΈ μ•„λ‹™λ‹ˆλ‹€. * localhost:3000 뢀뢄은 μ•Œμ•„μ„œ μˆ˜μ • ν•˜μ„Έμš”. */ export function makeBoardImgURL(data: any): string { try { console.log(`# mkburl data: `, data); let metaData: any = {}; metaData.dir = data?.imgurl ?? ""; metaData.mimetype = data?.minetype ?? ""; metaData = JSON.stringify(metaData); metaData = Buffer.from(metaData).toString("base64url"); let imgurl = `http://localhost:3000/api/stream/img?data=${metaData}`; return imgurl; } catch (error: any) { console.log(`# mkburl data err: `, data); return "/no_img.jpg"; } } /** * λ¬Έμžμ—΄μ΄ 일반적인 폴더/파일 경둜 ν˜•μ‹μΈμ§€ κ²€μ‚¬ν•©λ‹ˆλ‹€. * 이 μ •κ·œν‘œν˜„μ‹μ€ μ™„λ²½ν•œ μœ νš¨μ„± 검사(μ‹€μ œ 파일 μ‹œμŠ€ν…œ κ·œμΉ™)κ°€ μ•„λ‹Œ, * 경둜 ꡬ쑰(μŠ¬λž˜μ‹œ, 점, 파일λͺ… λ“±)λ₯Ό ν¬ν•¨ν•˜λŠ”μ§€ ν™•μΈν•˜λŠ” 데 쀑점을 λ‘‘λ‹ˆλ‹€. * @param pathString 검사할 λ¬Έμžμ—΄ * @returns 경둜 ν˜•μ‹μΈ 경우 true, μ•„λ‹ˆλ©΄ false */ export function isPathFormat(pathString: string): boolean { if (typeof pathString !== "string" || pathString.trim() === "") { return false; } // 포괄적인 경둜 ν˜•μ‹ μ •κ·œν‘œν˜„μ‹ // 1. λ“œλΌμ΄λΈŒ 문자 (C:\) λ˜λŠ” μœ λ‹‰μŠ€ 루트 (/)둜 μ‹œμž‘ // 2. 경둜 κ΅¬λΆ„μž (/, \)와 일반적인 문자(문자, 숫자, ν•˜μ΄ν”ˆ, 언더바, λ§ˆμΉ¨ν‘œ) 포함 // 3. UNC 경둜 (\\server\share)도 ν—ˆμš© const pathRegex = new RegExp( /^((?:[a-zA-Z]:)?[\\\/]|\.{1,2}[\\\/]?|(?:[a-zA-Z0-9_-]+\/|\\)+|(?:[a-zA-Z]:))?(?:[a-zA-Z0-9_\-.\s]+[\\\/]?)*[a-zA-Z0-9_\-.\s]+$/, "i" // λŒ€μ†Œλ¬Έμž ꡬ뢄 μ—†μŒ ); // κ²½λ‘œμ— '?'λ‚˜ '*' 같은 glob λ¬Έμžκ°€ ν¬ν•¨λœ 경우λ₯Ό λ‹¨μˆœ 경둜둜 κ°„μ£Όν•˜μ§€ μ•Šμ„ 수 μžˆμŠ΅λ‹ˆλ‹€. // μ—¬κΈ°μ„œλŠ” 일반적인 경둜 ν˜•μ‹λ§Œ ν™•μΈν•©λ‹ˆλ‹€. return pathRegex.test(pathString); }