File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import 'server-only';
import { backendFetch } from '@/lib/http/backend-fetch';
import { decodeJwtPayload } from '@/lib/auth/decode-jwt-payload';
import type { AdminUser } from '@/lib/domain/admin-user';
/**
* 관리자 인증 Repository — 백엔드(edupay-backend) 실연동.
*
* 엔드포인트: `POST /api/v1/common/auth/admin/login` (baseURL: lib/env.ts의 getApiBaseUrl()).
* 요청 필드명은 `loginPw`다(`password`가 아님 — 백엔드 계약 그대로).
*
* 백엔드에는 관리자 프로필 조회 API가 없다(2026-08 기준) — 그래서 로그인 응답으로 받은
* accessToken(JWT)의 payload를 디코딩해 AdminUser를 구성한다. 서명 검증은 하지 않는다
* (백엔드 시크릿이 없어 불가능 — lib/auth/decode-jwt-payload.ts 참고, 위조 방지는 이후
* 우리 세션 HMAC 서명이 담당한다). 관리자 "이름" 클레임이 토큰에 없어 loginId로 대체한다
* (사용자 확정 사항).
*/
export type AdminCredentials = {
loginId: string;
password: string;
};
export type AdminLoginResult = {
admin: AdminUser;
accessToken: string;
refreshToken: string;
/** 백엔드 accessToken의 `exp` 클레임(unix seconds) — 세션 만료를 이 값에 정렬시키는 데 쓴다. */
accessTokenExpiresAt: number;
};
/** 백엔드 accessToken(JWT)의 payload 클레임. */
type AdminAccessTokenClaims = {
sub: string;
loginId: string;
admRoleCd: string;
adminId: string;
userType: string;
userId: string;
iat: number;
exp: number;
};
type AdminLoginResponse = {
accessToken: string;
refreshToken: string;
};
/**
* 관리자 로그인을 시도한다. 자격 불일치·존재하지 않는 계정 등 인증 실패는 예외가 아니라
* 정상 흐름이므로 null을 반환한다(네트워크·파싱 등 진짜 예외 상황은 lib/http/backend-fetch.ts가
* 흡수해 동일하게 `ok:false`로 내려주므로 이 함수 입장에서는 실패 사유를 구분하지 않는다 —
* 호출부가 항상 일반화된 오류 메시지로 응답하기 때문에 구분할 필요가 없다).
*
* 캐시 전략: `no-store` — 인증 요청은 재사용 캐시 대상이 아니다(매 시도가 백엔드에 도달해야 함).
*/
export async function requestAdminLogin(
credentials: AdminCredentials
): Promise<AdminLoginResult | null> {
const result = await backendFetch<AdminLoginResponse>(
'/api/v1/common/auth/admin/login',
{
method: 'POST',
body: { loginId: credentials.loginId, loginPw: credentials.password },
cache: 'no-store',
}
);
if (!result.ok) {
return null;
}
const claims = decodeJwtPayload<AdminAccessTokenClaims>(result.data.accessToken);
if (
!claims ||
typeof claims.adminId !== 'string' ||
!claims.adminId ||
typeof claims.loginId !== 'string' ||
!claims.loginId ||
typeof claims.admRoleCd !== 'string' ||
!claims.admRoleCd ||
typeof claims.exp !== 'number'
) {
return null;
}
const admin: AdminUser = {
id: claims.adminId,
// 백엔드 accessToken에 관리자 이름 클레임이 없어 loginId로 대체한다(사용자 확정 사항).
name: claims.loginId,
loginId: claims.loginId,
roleCode: claims.admRoleCd,
};
return {
admin,
accessToken: result.data.accessToken,
refreshToken: result.data.refreshToken,
accessTokenExpiresAt: claims.exp,
};
}