summaryrefslogtreecommitdiff
path: root/lib/nonsap/auth-service.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-12-01 16:13:43 +0900
committerjoonhoekim <26rote@gmail.com>2025-12-01 16:13:43 +0900
commit41bb0f9f67a85ac8e17d766492f79a2997d3c6e9 (patch)
treea2d56ea5b4713fe3a762c234622570cb36729628 /lib/nonsap/auth-service.ts
parent13c8b4e48f62c1f437b1a2b10731d092fea2a83f (diff)
(김준회) 권한관리: 페이지 조회 권한 확인 처리
Diffstat (limited to 'lib/nonsap/auth-service.ts')
-rw-r--r--lib/nonsap/auth-service.ts175
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 };
+ }
+}