summaryrefslogtreecommitdiff
path: root/lib/users
diff options
context:
space:
mode:
Diffstat (limited to 'lib/users')
-rw-r--r--lib/users/auth/verifyCredentails.ts1
-rw-r--r--lib/users/service.ts176
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,
+ };
}
}