diff options
Diffstat (limited to 'lib/users')
| -rw-r--r-- | lib/users/auth/verifyCredentails.ts | 1 | ||||
| -rw-r--r-- | lib/users/service.ts | 176 |
2 files changed, 148 insertions, 29 deletions
diff --git a/lib/users/auth/verifyCredentails.ts b/lib/users/auth/verifyCredentails.ts index 83a2f276..a5dbab41 100644 --- a/lib/users/auth/verifyCredentails.ts +++ b/lib/users/auth/verifyCredentails.ts @@ -3,6 +3,7 @@ import bcrypt from 'bcryptjs'; import crypto from 'crypto'; +// (처리 불필요) 키 암호화를 위한 fs 모듈 사용, 형제 경로 사용하며 public 경로 아니므로 파일이 노출되지 않음. import fs from 'fs'; import path from 'path'; import { eq, and, desc, gte, count } from 'drizzle-orm'; diff --git a/lib/users/service.ts b/lib/users/service.ts index 7a635113..80c346fa 100644 --- a/lib/users/service.ts +++ b/lib/users/service.ts @@ -5,7 +5,6 @@ import { Otp } from '@/types/user'; import { getAllUsers, createUser, getUserById, updateUser, deleteUser, getUserByEmail, createOtp,getOtpByEmailAndToken, updateOtp, findOtpByEmail ,getOtpByEmailAndCode, findAllRoles, getRoleAssignedUsers} from './repository'; import logger from '@/lib/logger'; import { Role, roles, userRoles, users, userView, type User } from '@/db/schema/users'; -import { saveDocument } from '../storage'; import { GetSimpleUsersSchema, GetUsersSchema } from '../admin-users/validations'; import { revalidatePath, revalidateTag, unstable_cache, unstable_noStore } from 'next/cache'; import { filterColumns } from '../filter-columns'; @@ -15,6 +14,7 @@ import { getErrorMessage } from "@/lib/handle-error"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { and, or, desc, asc, ilike, eq, isNull, sql, count, inArray, ne } from "drizzle-orm"; +import { SaveFileResult, saveFile } from '../file-stroage'; interface AssignUsersArgs { roleId: number @@ -232,41 +232,159 @@ export async function findEmailTemp(email: string) { } } -export async function updateUserProfileImage(formData: FormData) { +/** + * 프로필 업데이트 결과 인터페이스 + */ +interface ProfileUpdateResult { + success: boolean; + user?: any; + error?: string; + duration?: number; + fileInfo?: { + fileName: string; + originalName: string; + fileSize: string; + publicPath: string; + }; + securityChecks?: { + extensionCheck: boolean; + fileNameCheck: boolean; + sizeCheck: boolean; + mimeTypeCheck: boolean; + contentCheck: boolean; + }; +} + +/** + * 사용자 데이터 검증 (프로필 업데이트용) + */ +const validateUserData = { + validateEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }, + + validateName(name: string): boolean { + return name.length >= 2 && + name.length <= 50 && + !/[<>:"'|?*]/.test(name); + }, +}; + +/** + * 파일 크기를 읽기 쉬운 형태로 변환 + */ +const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +}; + +/** + * 보안 강화된 사용자 프로필 이미지 업데이트 함수 + * (file-storage.ts의 saveFile 함수 활용) + */ +export async function updateUserProfileImage(formData: FormData): Promise<ProfileUpdateResult> { + const startTime = Date.now(); + // 1) FormData에서 데이터 꺼내기 - const file = formData.get("file") as File | null - const userId = Number(formData.get("userId")) - const name = formData.get("name") as string - const email = formData.get("email") as string - - // 2) 기본적인 유효성 검증 - if (!file) { - throw new Error("No file found in the FormData.") - } - if (!userId) { - throw new Error("userId is required.") - } + const file = formData.get("file") as File | null; + const userId = Number(formData.get("userId")); + const name = formData.get("name") as string; + const email = formData.get("email") as string; try { - // 3) 파일 저장 (해시 생성) - const directory = './public/profiles' - const { hashedFileName } = await saveDocument(file, directory) - - // 4) DB 업데이트 - const imageUrl = hashedFileName - const data = { name, email, imageUrl } - const user = await updateUser(userId, data) + // 2) 기본 유효성 검증 + if (!file) { + throw new Error("파일이 제공되지 않았습니다."); + } + + if (!userId || userId <= 0) { + throw new Error("유효한 사용자 ID가 필요합니다."); + } + + // 3) 사용자 정보 검증 (파일 저장 전에 미리 체크) + if (name && !validateUserData.validateName(name)) { + throw new Error("유효하지 않은 이름입니다 (2-50자, 특수문자 제한)."); + } + + if (email && !validateUserData.validateEmail(email)) { + throw new Error("유효하지 않은 이메일 주소입니다."); + } + + console.log("📤 프로필 이미지 업데이트 시작:", { + fileName: file.name, + fileSize: formatFileSize(file.size), + userId, + }); + + // 4) file-storage.ts의 saveFile 함수로 보안 검증 + 저장 (한 번에 처리!) + const saveResult: SaveFileResult = await saveFile({ + file, + directory: 'profiles', // './public/profiles'에서 'profiles'로 변경 + originalName: file.name, + userId: userId.toString(), + }); + + // 5) 파일 저장 실패 시 에러 처리 + if (!saveResult.success) { + throw new Error(saveResult.error || "파일 저장에 실패했습니다."); + } + + console.log("✅ 파일 저장 성공:", { + fileName: saveResult.fileName, + publicPath: saveResult.publicPath, + securityChecks: saveResult.securityChecks, + }); + + // 6) DB 업데이트 (file-storage.ts에서 반환된 publicPath 사용) + const imageUrl = saveResult.publicPath; // 웹에서 접근 가능한 경로 + const data = { name, email, imageUrl }; + const user = await updateUser(userId, data); + if (!user) { - // updateUser가 null을 반환하면, DB 업데이트 실패 혹은 해당 유저가 없음 - throw new Error(`User with id=${userId} not found or update failed.`) + // DB 업데이트 실패 시 업로드된 파일 정리 + // TODO: file-storage.ts의 deleteFile 함수 사용하여 파일 삭제 고려 + throw new Error(`사용자 ID=${userId}를 찾을 수 없거나 업데이트에 실패했습니다.`); } - // 5) 성공 시 성공 정보 반환 - return { success: true, user } + // 7) 성공 반환 + const duration = Date.now() - startTime; + + console.log("🎉 프로필 업데이트 성공:", { + userId, + imageUrl, + duration: `${duration}ms`, + }); + + return { + success: true, + user, + duration, + fileInfo: { + fileName: saveResult.fileName!, + originalName: saveResult.originalName!, + fileSize: formatFileSize(saveResult.fileSize!), + publicPath: saveResult.publicPath!, + }, + securityChecks: saveResult.securityChecks, + }; + } catch (err: any) { - // DB 업데이트 중 발생하는 에러나 saveDocument 내부 에러 등을 처리 - console.error("[updateUserProfileImage] Error:", err) - throw new Error(err.message ?? "Failed to update user profile.") + const duration = Date.now() - startTime; + const errorMessage = err.message ?? "프로필 업데이트 중 오류가 발생했습니다."; + + console.error("❌ [updateUserProfileImage] Error:", err); + + return { + success: false, + error: errorMessage, + duration, + }; } } |
