summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/engineering/(engineering)/tech-project-avl/page.tsx85
-rw-r--r--app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx5
-rw-r--r--app/[lng]/evcp/(evcp)/tech-vendor-possible-items/page.tsx6
-rw-r--r--app/[lng]/pending/page.tsx2
-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
8 files changed, 130 insertions, 126 deletions
diff --git a/app/[lng]/engineering/(engineering)/tech-project-avl/page.tsx b/app/[lng]/engineering/(engineering)/tech-project-avl/page.tsx
deleted file mode 100644
index 21bc7b5d..00000000
--- a/app/[lng]/engineering/(engineering)/tech-project-avl/page.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import * as React from "react"
-import { redirect } from "next/navigation"
-import { getServerSession } from "next-auth/next"
-import { authOptions } from "@/app/api/auth/[...nextauth]/route"
-import { SearchParams } from "@/types/table"
-import { searchParamsCache } from "@/lib/tech-project-avl/validations"
-import { Skeleton } from "@/components/ui/skeleton"
-import { Shell } from "@/components/shell"
-import { AcceptedQuotationsTable } from "@/lib/tech-project-avl/table/accepted-quotations-table"
-import { getAcceptedTechSalesVendorQuotations } from "@/lib/techsales-rfq/service"
-import { getValidFilters } from "@/lib/data-table"
-import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
-import { Ellipsis } from "lucide-react"
-
-export interface PageProps {
- params: Promise<{ lng: string }>
- searchParams: Promise<SearchParams>
-}
-
-export default async function AcceptedQuotationsPage({
- params,
- searchParams,
-}: PageProps) {
- const { lng } = await params
-
- const session = await getServerSession(authOptions)
- if (!session) {
- redirect(`/${lng}/auth/signin`)
- }
-
- const search = await searchParams
- const { page, perPage, sort, filters, search: searchText } = searchParamsCache.parse(search)
- const validFilters = getValidFilters(filters ?? [])
-
- const { data, pageCount } = await getAcceptedTechSalesVendorQuotations({
- page,
- perPage: perPage ?? 10,
- sort,
- search: searchText,
- filters: validFilters,
- })
-
- return (
- <Shell className="gap-2">
- <div className="flex items-center justify-between space-y-2">
- <div className="flex items-center justify-between space-y-2">
- <div>
- <h2 className="text-2xl font-bold tracking-tight">
- 승인된 견적서(해양TOP,HULL) 관리
- </h2>
- {/* <p className="text-muted-foreground">
- 기술영업 승인 견적서에 대한 요약 정보를 확인하고{" "}
- <span className="inline-flex items-center whitespace-nowrap">
- <Ellipsis className="size-3" />
- <span className="ml-1">버튼</span>
- </span>
- 을 통해 RFQ 코드, 설명, 업체명, 업체 코드 등의 상세 정보를 확인할 수 있습니다.
- </p> */}
- </div>
- </div>
- </div>
-
- <React.Suspense fallback={<Skeleton className="h-7 w-52" />}>
- {/* Date range picker can be added here if needed */}
- </React.Suspense>
-
- <React.Suspense
- fallback={
- <DataTableSkeleton
- columnCount={12}
- searchableColumnCount={2}
- filterableColumnCount={4}
- cellWidths={["10rem", "15rem", "12rem", "10rem", "10rem", "12rem", "8rem", "12rem", "10rem", "8rem", "10rem", "10rem"]}
- shrinkZero
- />
- }
- >
- <AcceptedQuotationsTable
- data={data}
- pageCount={pageCount}
- />
- </React.Suspense>
- </Shell>
- )
-}
diff --git a/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx b/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx
index 886d061d..325037d8 100644
--- a/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx
+++ b/app/[lng]/evcp/(evcp)/evaluation-target-list/page.tsx
@@ -10,6 +10,9 @@ import { getEvaluationTargets } from "@/lib/evaluation-target-list/service"
import { InformationButton } from "@/components/information/information-button"
import { EvaluationTargetsTable } from "@/lib/evaluation-target-list/table/evaluation-target-table"
+export const dynamic = 'force-dynamic'
+export const revalidate = 0
+
export const metadata: Metadata = {
title: "협력업체 평가 대상 관리",
description: "협력업체 평가 대상을 확정하고 담당자를 지정합니다.",
@@ -50,7 +53,7 @@ export default async function EvaluationTargetsPage(props: EvaluationTargetsPage
{/* Main Table */}
<React.Suspense
- key={`evaluation-targets-${search.page}-${JSON.stringify(search.filters)}-${search.joinOperator}-${search.search || 'no-search'}`}
+ // key={`evaluation-targets-${search.page}-${JSON.stringify(search.filters)}-${search.joinOperator}-${search.search || 'no-search'}`}
fallback={
<DataTableSkeleton
columnCount={12}
diff --git a/app/[lng]/evcp/(evcp)/tech-vendor-possible-items/page.tsx b/app/[lng]/evcp/(evcp)/tech-vendor-possible-items/page.tsx
index 00192d85..51ff6003 100644
--- a/app/[lng]/evcp/(evcp)/tech-vendor-possible-items/page.tsx
+++ b/app/[lng]/evcp/(evcp)/tech-vendor-possible-items/page.tsx
@@ -6,7 +6,7 @@ import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
import { Shell } from "@/components/shell"
import { searchParamsTechVendorPossibleItemsCache } from "@/lib/tech-vendor-possible-items/validations"
-import { getTechVendorPossibleItems, getUniqueTechVendorTypes } from "@/lib/tech-vendor-possible-items/service"
+import { getTechVendorPossibleItems } from "@/lib/tech-vendor-possible-items/service"
import { PossibleItemsDataTable } from "@/lib/tech-vendor-possible-items/table/possible-items-data-table"
import { TechVendorPossibleItemsContainer } from "@/components/tech-vendor-possible-items/tech-vendor-possible-items-container"
@@ -31,9 +31,9 @@ export default async function TechVendorPossibleItemsPage(props: TechVendorPossi
const promises = Promise.all([
getTechVendorPossibleItems({
...search,
- filters: validFilters,
+ filters: validFilters as any,
}),
- getUniqueTechVendorTypes(),
+ // getUniqueTechVendorTypes(),
])
return (
diff --git a/app/[lng]/pending/page.tsx b/app/[lng]/pending/page.tsx
index 0800e5d2..b3818873 100644
--- a/app/[lng]/pending/page.tsx
+++ b/app/[lng]/pending/page.tsx
@@ -15,6 +15,8 @@ import { getServerSession } from "next-auth/next"
export default async function PendingPage() {
const session = await getServerSession(authOptions)
+ console.log(session)
+
return (
<div className="max-w-4xl mx-auto space-y-8">
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}`)