import 'server-only'; import { cache } from 'react'; import { redirect } from 'next/navigation'; import { readSessionToken } from '@/lib/auth/session'; import { verifySessionToken } from '@/lib/auth/session-token'; import type { AdminUser } from '@/lib/domain/admin-user'; /** * 공식 가이드의 Data Access Layer(DAL) — 인증·인가 판단을 이 파일에 집중시킨다. * `cache()`로 렌더 패스당 세션 조회를 1회로 dedupe한다. * * 이후 모든 mutation(Server Action)은 Repository를 직접 호출하기 전에 반드시 * `verifySession()`(또는 `getSessionAdmin()`)을 거쳐야 한다 — Server Action은 UI를 거치지 않고 * 직접 호출될 수 있으므로 이 검증이 유일한 최종 방어선이다. * * 백엔드에 관리자 프로필 조회 API가 없어(2026-08 기준) 세션 payload에 이미 실려 있는 클레임 * (adminId/loginId/admRoleCd)만으로 AdminUser를 구성한다 — Repository 왕복이 없다. */ /** 세션 검증 결과를 null 허용으로 반환 — redirect 없이 상태만 알고 싶을 때 사용 (예: 로그인 페이지). */ export const getSessionAdmin = cache(async (): Promise => { const token = await readSessionToken(); if (!token) { return null; } const session = verifySessionToken(token); if (!session) { return null; } return { id: session.adminId, // 백엔드 accessToken에 관리자 이름 클레임이 없어 loginId로 대체한다(사용자 확정 사항). name: session.loginId, loginId: session.loginId, roleCode: session.admRoleCd, }; }); /** * 백엔드 호출에 부착할 accessToken을 세션에서 꺼낸다 — **서버 전용, 화면으로 내려보내지 않는다.** * * `getSessionAdmin()`이 반환하는 `AdminUser`에는 토큰이 없다(민감 값이라 화면 DTO에서 의도적으로 * 제외). 그래서 백엔드 API를 부르는 Repository용 경로를 따로 둔다. 세션이 없거나 만료·위조면 * null이며, 그 경우 호출부는 Authorization 없이 요청하게 되고 백엔드가 401로 거절한다(fail-closed). */ export const getSessionAccessToken = cache(async (): Promise => { const token = await readSessionToken(); if (!token) { return null; } return verifySessionToken(token)?.accessToken ?? null; }); /** 보호 라우트·Server Action 진입점 — 세션이 없으면 `/login`으로 redirect한다. */ export async function verifySession(): Promise { const admin = await getSessionAdmin(); if (!admin) { redirect('/login'); } return admin; }