diff options
| author | joonhoekim <26rote@gmail.com> | 2025-12-01 16:13:43 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-12-01 16:13:43 +0900 |
| commit | 41bb0f9f67a85ac8e17d766492f79a2997d3c6e9 (patch) | |
| tree | a2d56ea5b4713fe3a762c234622570cb36729628 /lib/nonsap/auth-service.ts | |
| parent | 13c8b4e48f62c1f437b1a2b10731d092fea2a83f (diff) | |
(김준회) 권한관리: 페이지 조회 권한 확인 처리
Diffstat (limited to 'lib/nonsap/auth-service.ts')
| -rw-r--r-- | lib/nonsap/auth-service.ts | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/lib/nonsap/auth-service.ts b/lib/nonsap/auth-service.ts new file mode 100644 index 00000000..5a338eea --- /dev/null +++ b/lib/nonsap/auth-service.ts @@ -0,0 +1,175 @@ +import { headers } from 'next/headers'; +import db from '@/db/db'; +import { users } from '@/db/schema/users'; +import { organization } from '@/db/schema/knox/organization'; +import { eq } from 'drizzle-orm'; +import { + getAllScreens, + getAuthsByScreenId, + getUserRoles, +} from './db'; + +export type AuthAction = 'SEARCH' | 'ADD' | 'DEL' | 'SAVE' | 'PRINT' | 'DOWN' | 'UP' | 'APPROVAL' | 'PREV' | 'NEXT'; + +interface AuthCheckResult { + authorized: boolean; + message?: string; +} + +async function getCurrentUrlFromHeaders(): Promise<string | null> { + const headerList = headers(); + return headerList.get('x-pathname') || null; +} + +/** + * NONSAP 권한 검증 함수 + * @param userId 사용자 ID (Postgres users.id) + * @param requiredActions 필요한 권한 목록 (예: ['SEARCH']) + * @param url (Optional) 검증할 URL. 생략 시 현재 요청의 URL을 자동으로 감지 시도. + */ +export async function verifyNonsapPermission( + userId: number, + requiredActions: AuthAction[], + url?: string, + options: { logic?: 'AND' | 'OR' } = { logic: 'AND' } +): Promise<AuthCheckResult> { + // 1. URL 결정 + const targetUrl = url || await getCurrentUrlFromHeaders(); + if (!targetUrl) { + return { authorized: false, message: "URL을 확인할 수 없습니다. URL 인자를 명시해주세요." }; + } + + // URL 정규화 (언어 코드 제거) + // 예: /en/partners/dashboard -> /partners/dashboard + const normalizedUrl = targetUrl.replace(/^\/[a-z]{2}(\/|$)/, '/'); + + // 2. 화면 식별 + const allScreens = await getAllScreens(); + + // DB URL과 현재 URL 매칭 (Longest Prefix Match) + const screen = allScreens + .filter(s => { + const dbUrl = s.SCR_URL.startsWith('/') ? s.SCR_URL : `/${s.SCR_URL}`; + // 정확히 일치하거나 하위 경로인 경우 + return normalizedUrl === dbUrl || normalizedUrl.startsWith(`${dbUrl}/`); + }) + .sort((a, b) => b.SCR_URL.length - a.SCR_URL.length)[0]; + + if (!screen) { + // 관리되지 않는 페이지 -> Pass + return { authorized: true, message: "관리되지 않는 페이지입니다." }; + } + + if (screen.DEL_YN === 'Y') { + return { authorized: false, message: "삭제된 화면입니다." }; + } + + if (screen.SCRT_CHK_YN === 'N') { + return { authorized: true, message: "권한 체크가 필요 없는 화면입니다." }; + } + + // 3. 사용자 정보 조회 (Postgres) + const user = await db.query.users.findFirst({ + where: eq(users.id, userId), + columns: { + nonsapUserId: true, + deptCode: true, + employeeNumber: true, + } + }); + + if (!user) { + return { authorized: false, message: "사용자 정보를 찾을 수 없습니다." }; + } + + // 4. 권한 정보 조회 + const auths = await getAuthsByScreenId(screen.SCR_ID); + + // 5. 권한 검증 (Merging) + const finalAuth = { + SEARCH: 'N', ADD: 'N', DEL: 'N', SAVE: 'N', PRINT: 'N', DOWN: 'N', UP: 'N', APPROVAL: 'N', PREV: 'N', NEXT: 'N' + }; + + // Role 정보 조회 + const myRoles = await getUserRoles(user.employeeNumber || user.nonsapUserId || ''); + + for (const auth of auths) { + let isMatch = false; + + if (auth.ACSR_GB_CD === 'U') { + // 사용자 직접 매핑 + if (auth.ACSR_ID === user.nonsapUserId) isMatch = true; + } else if (auth.ACSR_GB_CD === 'R') { + // 역할 매핑 + if (myRoles.some(r => r.ROLE_ID === auth.ACSR_ID)) isMatch = true; + } else if (auth.ACSR_GB_CD === 'D') { + // 부서 매핑 (정확히 일치) + if (auth.ACSR_ID === user.deptCode) isMatch = true; + } else if (auth.ACSR_GB_CD === 'E') { + // 부서 매핑 (하위 포함 - 상위 부서로 거슬러 올라가며 확인) + if (user.deptCode) { + let currentDept = user.deptCode; + let depth = 0; + // 최대 20단계까지만 확인 (무한루프 방지) + while (currentDept && depth < 20) { + if (currentDept === auth.ACSR_ID) { + isMatch = true; + break; + } + + // 상위 부서 조회 + // 성능 최적화를 위해 캐싱 고려 가능하나, 일단 직접 조회 + const org = await db.query.organization.findFirst({ + where: eq(organization.departmentCode, currentDept), + columns: { uprDepartmentCode: true } + }); + + if (!org || !org.uprDepartmentCode || org.uprDepartmentCode === 'TOP' || org.uprDepartmentCode === currentDept) { + break; + } + currentDept = org.uprDepartmentCode; + depth++; + } + } + } + + if (isMatch) { + // 권한 합치기 (OR) + if (auth.AUTH_CD_SEARCH === 'Y') finalAuth.SEARCH = 'Y'; + if (auth.AUTH_CD_ADD === 'Y') finalAuth.ADD = 'Y'; + if (auth.AUTH_CD_DEL === 'Y') finalAuth.DEL = 'Y'; + if (auth.AUTH_CD_SAVE === 'Y') finalAuth.SAVE = 'Y'; + if (auth.AUTH_CD_PRINT === 'Y') finalAuth.PRINT = 'Y'; + if (auth.AUTH_CD_DOWN === 'Y') finalAuth.DOWN = 'Y'; + if (auth.AUTH_CD_UP === 'Y') finalAuth.UP = 'Y'; + if (auth.AUTH_CD_APPROVAL === 'Y') finalAuth.APPROVAL = 'Y'; + if (auth.AUTH_CD_PREV === 'Y') finalAuth.PREV = 'Y'; + if (auth.AUTH_CD_NEXT === 'Y') finalAuth.NEXT = 'Y'; + } + } + + // 6. 요구사항 검증 + const check = (action: AuthAction) => { + switch (action) { + case 'SEARCH': return finalAuth.SEARCH === 'Y'; + case 'ADD': return finalAuth.ADD === 'Y'; + case 'DEL': return finalAuth.DEL === 'Y'; + case 'SAVE': return finalAuth.SAVE === 'Y'; + case 'PRINT': return finalAuth.PRINT === 'Y'; + case 'DOWN': return finalAuth.DOWN === 'Y'; + case 'UP': return finalAuth.UP === 'Y'; + case 'APPROVAL': return finalAuth.APPROVAL === 'Y'; + case 'PREV': return finalAuth.PREV === 'Y'; + case 'NEXT': return finalAuth.NEXT === 'Y'; + default: return false; + } + }; + + if (options.logic === 'OR') { + const authorized = requiredActions.some(action => check(action)); + return { authorized }; + } else { + const authorized = requiredActions.every(action => check(action)); + return { authorized }; + } +} |
