diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-25 07:51:15 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-25 07:51:15 +0000 |
| commit | 2650b7c0bb0ea12b68a58c0439f72d61df04b2f1 (patch) | |
| tree | 17156183fd74b69d78178065388ac61a18ac07b4 /app/api | |
| parent | d32acea05915bd6c1ed4b95e56c41ef9204347bc (diff) | |
(대표님) 정기평가 대상, 미들웨어 수정, nextauth 토큰 처리 개선, GTC 등
(최겸) 기술영업
Diffstat (limited to 'app/api')
| -rw-r--r-- | app/api/auth/[...nextauth]/route.ts | 89 | ||||
| -rw-r--r-- | app/api/auth/util.ts | 52 | ||||
| -rw-r--r-- | app/api/files/[...path]/route.ts | 2 | ||||
| -rw-r--r-- | app/api/revision-upload-ship/route.ts | 15 |
4 files changed, 121 insertions, 37 deletions
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 68cc3a5b..fe93906d 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -89,9 +89,9 @@ let securitySettingsCache: { async function getCachedSecuritySettings() { const now = Date.now() - - if (!securitySettingsCache.data || - (now - securitySettingsCache.lastFetch) > securitySettingsCache.ttl) { + + if (!securitySettingsCache.data || + (now - securitySettingsCache.lastFetch) > securitySettingsCache.ttl) { try { securitySettingsCache.data = await getSecuritySettings() securitySettingsCache.lastFetch = now @@ -102,7 +102,7 @@ async function getCachedSecuritySettings() { } } } - + return securitySettingsCache.data } @@ -110,15 +110,15 @@ async function getCachedSecuritySettings() { function getClientIP(req: any): string { const forwarded = req.headers['x-forwarded-for'] const realIP = req.headers['x-real-ip'] - + if (forwarded) { return forwarded.split(',')[0].trim() } - + if (realIP) { return realIP } - + return req.ip || req.connection?.remoteAddress || '127.0.0.1' } @@ -212,7 +212,7 @@ export const authOptions: NextAuthOptions = { // DB에 로그인 세션 생성 const ipAddress = getClientIP(req) const userAgent = req.headers?.['user-agent'] - + const dbSession = await SessionRepository.createLoginSession({ userId: user.id, ipAddress, @@ -243,7 +243,7 @@ export const authOptions: NextAuthOptions = { } }, }), - + // 1차 인증용 프로바이더 (기존 유지) CredentialsProvider({ id: 'credentials-first-auth', @@ -283,6 +283,7 @@ export const authOptions: NextAuthOptions = { callbacks: { // ✅ JWT callback에 roles 정보 추가 + // JWT callback 수정 async jwt({ token, user, account, trigger, session }) { const securitySettings = await getCachedSecuritySettings() const sessionTimeoutMs = securitySettings.sessionTimeoutMinutes * 60 * 1000 @@ -301,19 +302,50 @@ export const authOptions: NextAuthOptions = { token.authMethod = user.authMethod token.sessionExpiredAt = reAuthTime + sessionTimeoutMs token.dbSessionId = user.dbSessionId - token.roles = user.roles // ✅ roles 정보 추가 + token.roles = user.roles + } + + // ✅ 기존 토큰이 있고 로그인이 아닌 경우, DB에서 최신 사용자 정보 조회 + if (token.id && !user && trigger !== "update") { + try { + const latestUser = await getUserById(parseInt(token.id)) + + if (latestUser) { + // 도메인이 변경되었다면 토큰 업데이트 + if (token.domain !== latestUser.domain) { + console.log(`Domain changed for user ${token.email}: ${token.domain} -> ${latestUser.domain}`) + token.domain = latestUser.domain + } + + // 기타 정보도 최신 상태로 업데이트 + token.name = latestUser.name + token.companyId = latestUser.companyId + token.techCompanyId = latestUser.techCompanyId + + // roles 정보도 최신으로 업데이트 + const userRoles = await getUserRoles(parseInt(token.id)) + token.roles = userRoles + } + } catch (error) { + console.error('Failed to fetch latest user info in JWT callback:', error) + } } // SAML 인증 시 DB 세션 생성 및 roles 조회 if (account && account.provider === 'credentials-saml' && token.id) { const reAuthTime = Date.now() const sessionExpiredAt = new Date(reAuthTime + sessionTimeoutMs) - + try { const numericUserId = ensureNumber(token.id) - - // ✅ SAML 로그인 시에도 roles 정보 조회 - if (!token.roles) { + + // SAML 로그인 시에도 최신 사용자 정보 조회 + const latestUser = await getUserById(numericUserId) + if (latestUser) { + token.domain = latestUser.domain + token.name = latestUser.name + token.companyId = latestUser.companyId + token.techCompanyId = latestUser.techCompanyId token.roles = await getUserRoles(numericUserId) } @@ -323,7 +355,7 @@ export const authOptions: NextAuthOptions = { authMethod: 'saml', sessionExpiredAt, }) - + token.authMethod = 'saml' token.reAuthTime = reAuthTime token.sessionExpiredAt = reAuthTime + sessionTimeoutMs @@ -338,7 +370,7 @@ export const authOptions: NextAuthOptions = { if (session.reAuthTime !== undefined) { token.reAuthTime = session.reAuthTime token.sessionExpiredAt = session.reAuthTime + sessionTimeoutMs - + // DB 세션 업데이트 if (token.dbSessionId) { await SessionRepository.updateLoginSession(token.dbSessionId, { @@ -347,28 +379,31 @@ export const authOptions: NextAuthOptions = { }) } } - + if (session.user) { if (session.user.name !== undefined) token.name = session.user.name if (session.user.email !== undefined) token.email = session.user.email if (session.user.image !== undefined) token.imageUrl = session.user.image + + // ✅ 세션 업데이트 시 도메인 정보도 갱신 가능 + if (session.user.domain !== undefined) token.domain = session.user.domain } } return token - }, - + } +, // ✅ Session callback에 roles 정보 추가 async session({ session, token }: { session: Session; token: JWT }) { // 세션 만료 체크 if (token.sessionExpiredAt && Date.now() > token.sessionExpiredAt) { console.log(`Session expired for user ${token.email}. Expired at: ${new Date(token.sessionExpiredAt)}`) - + // DB 세션 만료 처리 if (token.dbSessionId) { await SessionRepository.logoutSession(token.dbSessionId) } - + return { expires: new Date(0).toISOString(), user: null as any @@ -414,17 +449,17 @@ export const authOptions: NextAuthOptions = { async signIn({ user, account, profile }) { const securitySettings = await getCachedSecuritySettings() console.log(`User ${user.email} signed in via ${account?.provider} (authMethod: ${user.authMethod}), session timeout: ${securitySettings.sessionTimeoutMinutes} minutes`); - + // 이미 MFA에서 DB 세션이 생성된 경우가 아니라면 여기서 생성 if (account?.provider !== 'credentials-mfa' && user.id) { try { const numericUserId = ensureNumber(user.id) - + // 기존 활성 세션 확인 const existingSession = await SessionRepository.getActiveSessionByUserId(numericUserId) if (!existingSession) { const sessionExpiredAt = new Date(Date.now() + (securitySettings.sessionTimeoutMinutes * 60 * 1000)) - + await SessionRepository.createLoginSession({ userId: numericUserId, ipAddress: '0.0.0.0', @@ -437,14 +472,14 @@ export const authOptions: NextAuthOptions = { } } }, - + async signOut({ session, token }) { console.log(`User ${session?.user?.email || token?.email} signed out`); - + // DB에서 세션 로그아웃 처리 const userId = session?.user?.id || token?.id const dbSessionId = session?.user?.dbSessionId || token?.dbSessionId - + if (dbSessionId) { await SessionRepository.logoutSession(dbSessionId) } else if (userId) { diff --git a/app/api/auth/util.ts b/app/api/auth/util.ts new file mode 100644 index 00000000..9163c1c5 --- /dev/null +++ b/app/api/auth/util.ts @@ -0,0 +1,52 @@ +import { signOut } from 'next-auth/react' + +export async function completeLogout() { + // 1. NextAuth 로그아웃 + await signOut({ redirect: false }) + + // 2. 모든 NextAuth 관련 쿠키 제거 + const cookies = [ + 'next-auth.session-token', + '__Secure-next-auth.session-token', + 'next-auth.csrf-token', + '__Host-next-auth.csrf-token', + 'next-auth.callback-url', + '__Secure-next-auth.callback-url' + ] + + cookies.forEach(cookieName => { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure` + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${window.location.hostname}` + }) + + // 3. 로컬 스토리지와 세션 스토리지 클리어 + localStorage.clear() + sessionStorage.clear() + + // 4. 강제 페이지 리로드로 모든 캐시 제거 + window.location.href = '/ko/evcp' +} + +// 사용자 도메인 변경 후 호출할 함수 +export async function refreshUserSession() { + try { + const response = await fetch('/api/auth/refresh-user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + + if (response.ok) { + // 성공하면 페이지 리로드 + window.location.reload() + } else { + // 실패하면 완전 로그아웃 + await completeLogout() + } + } catch (error) { + console.error('Failed to refresh session:', error) + await completeLogout() + } +}
\ No newline at end of file diff --git a/app/api/files/[...path]/route.ts b/app/api/files/[...path]/route.ts index c9d530de..81f4b95d 100644 --- a/app/api/files/[...path]/route.ts +++ b/app/api/files/[...path]/route.ts @@ -42,7 +42,7 @@ const isAllowedPath = (requestedPath: string): boolean => { 'vendor-investigation', 'vendor-responses', 'vendor-evaluation', - 'vendor-evaluation-submit', + 'evaluation-attachments', 'vendor-attachments', ]; diff --git a/app/api/revision-upload-ship/route.ts b/app/api/revision-upload-ship/route.ts index 38762e5d..3d1ebba9 100644 --- a/app/api/revision-upload-ship/route.ts +++ b/app/api/revision-upload-ship/route.ts @@ -11,7 +11,7 @@ import { import { and, eq } from "drizzle-orm" /* 보안 강화된 파일 저장 유틸리티 */ -import { saveFile, SaveFileResult } from "@/lib/file-stroage" +import { saveFile, SaveFileResult, saveFileStream } from "@/lib/file-stroage" /* change log 유틸 */ import { @@ -42,11 +42,11 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "No files provided" }, { status: 400 }) // 기본 파일 크기 검증 (보안 함수에서도 검증하지만 조기 체크) - const MAX = 50 * 1024 * 1024 // 50MB (다이얼로그 제한과 맞춤) + const MAX = 1024 * 1024 * 1024 for (const f of attachmentFiles) { if (f.size > MAX) { return NextResponse.json( - { error: `${f.name} exceeds 50MB limit` }, + { error: `${f.name} exceeds 1GB limit` }, { status: 400 } ) } @@ -197,12 +197,9 @@ export async function POST(request: NextRequest) { console.log(`🔐 보안 검증 시작: ${file.name}`) // 보안 강화된 파일 저장 - const saveResult: SaveFileResult = await saveFile({ - file, - directory: "documents", // 문서 전용 디렉토리 - originalName: file.name, - userId: uploaderName || "anonymous", // 업로더 정보 로깅용 - }) + const saveResult = file.size > 100 * 1024 * 1024 + ? await saveFileStream({ file, directory: "documents", userId: uploaderName ||""}) + : await saveFile({ file, directory: "documents", userId: uploaderName ||"" }) if (!saveResult.success) { console.error(`❌ 파일 보안 검증 실패: ${file.name} - ${saveResult.error}`) |
