summaryrefslogtreecommitdiff
path: root/app/api
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-25 07:51:15 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-25 07:51:15 +0000
commit2650b7c0bb0ea12b68a58c0439f72d61df04b2f1 (patch)
tree17156183fd74b69d78178065388ac61a18ac07b4 /app/api
parentd32acea05915bd6c1ed4b95e56c41ef9204347bc (diff)
(대표님) 정기평가 대상, 미들웨어 수정, nextauth 토큰 처리 개선, GTC 등
(최겸) 기술영업
Diffstat (limited to 'app/api')
-rw-r--r--app/api/auth/[...nextauth]/route.ts89
-rw-r--r--app/api/auth/util.ts52
-rw-r--r--app/api/files/[...path]/route.ts2
-rw-r--r--app/api/revision-upload-ship/route.ts15
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}`)