68f85f3736
- 첫 로그인 비번 강제 변경 (RUN_082): FORCE_PASSWORD_CHANGE 컬럼, ForcePasswordChangeGuardFilter, /auth/change-password API + 페이지 - 테넌트 일관성 가드: TenantConsistencyGuardFilter 로 JWT.company_code ↔ 서브도메인 company_code 대조, CompanyResolver 가 (db_name, company_code) 동시 반환 - 회사 관리 확장 (RUN_083 audit log, RUN_084 lifecycle 컬럼): CompanyAdmin/Members/Templates/Lifecycle/AuditLog 서비스 + CompanyMgmtController + SuperAdminGuard - 회사 관리 UI: CompanyAccordionRow 탭화 + 모달 4종 (AdminInfo/Deactivate/Delete/RecopyTemplates) + AuditLogDrawer + csvExport - 프로비저닝 마법사: force_password_change 토글 반영 - 프론트 인증: storage 이벤트 멀티탭 동기화, 403 errorCode (PASSWORD_CHANGE_REQUIRED / CROSS_TENANT_REJECTED / TENANT_NOT_RESOLVED) 전역 리다이렉트 - 기타: StartupSchemaMigrator, OS별 도커 기동 스크립트, CLAUDE.md 트래킹 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
3.7 KiB
TypeScript
120 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, ReactNode } from "react";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { AuthLogger } from "@/lib/authLogger";
|
|
import { Loader2 } from "lucide-react";
|
|
|
|
interface AuthGuardProps {
|
|
children: ReactNode;
|
|
requireAuth?: boolean;
|
|
requireAdmin?: boolean;
|
|
redirectTo?: string;
|
|
fallback?: ReactNode;
|
|
}
|
|
|
|
/**
|
|
* 인증 보호 컴포넌트
|
|
* 로그인 상태 및 권한에 따라 접근을 제어
|
|
* - 토큰 갱신/401 처리는 client.ts 인터셉터가 담당
|
|
* - 이 컴포넌트는 인증 상태 기반 라우팅 가드 역할만 수행
|
|
*/
|
|
export function AuthGuard({
|
|
children,
|
|
requireAuth = true,
|
|
requireAdmin = false,
|
|
redirectTo = "/login",
|
|
fallback,
|
|
}: AuthGuardProps) {
|
|
const { isLoggedIn, isAdmin, loading, forcePasswordChange } = useAuth();
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
|
|
useEffect(() => {
|
|
if (loading) return;
|
|
|
|
// loading=false 인데 토큰만 localStorage 에 남아있고 isLoggedIn=false 라면
|
|
// useAuth 가 refreshUserData 에서 이미 removeToken 을 실행했어야 함.
|
|
// 혹시라도 외부가 stale 토큰을 set 해 놓은 경우엔 여기서 stuck 되지 않도록
|
|
// 그냥 리다이렉트로 진행시킨다. (이전 early-return 은 영구 stuck 을 유발했음)
|
|
|
|
if (requireAuth && !isLoggedIn) {
|
|
AuthLogger.log("AUTH_GUARD_BLOCK", `인증 필요하지만 비로그인 상태 → ${redirectTo} 리다이렉트`);
|
|
router.push(redirectTo);
|
|
return;
|
|
}
|
|
|
|
// 로그인은 됐지만 비밀번호 강제 변경 대기 — /change-password 외 경로는 모두 막음
|
|
if (isLoggedIn && forcePasswordChange && pathname !== "/change-password") {
|
|
AuthLogger.log("AUTH_GUARD_BLOCK", `force_password_change=true → /change-password 강제 이동`);
|
|
router.push("/change-password");
|
|
return;
|
|
}
|
|
|
|
if (requireAdmin && !isAdmin) {
|
|
AuthLogger.log("AUTH_GUARD_BLOCK", `관리자 권한 필요하지만 일반 사용자 → ${redirectTo} 리다이렉트`);
|
|
router.push(redirectTo);
|
|
return;
|
|
}
|
|
}, [requireAuth, requireAdmin, loading, isLoggedIn, isAdmin, forcePasswordChange, pathname, redirectTo, router]);
|
|
|
|
if (loading) {
|
|
return (
|
|
fallback || (
|
|
<div className="flex h-screen items-center justify-center">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
<p className="text-sm text-muted-foreground">로딩 중...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
);
|
|
}
|
|
|
|
if (requireAuth && !isLoggedIn) {
|
|
return (
|
|
fallback || (
|
|
<div className="flex h-screen items-center justify-center">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
<p className="text-sm text-muted-foreground">인증 확인 중...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
);
|
|
}
|
|
|
|
if (requireAdmin && !isAdmin) {
|
|
return (
|
|
fallback || (
|
|
<div className="flex h-screen items-center justify-center">
|
|
<p className="text-sm text-muted-foreground">관리자 권한이 필요합니다.</p>
|
|
</div>
|
|
)
|
|
);
|
|
}
|
|
|
|
return <>{children}</>;
|
|
}
|
|
|
|
/**
|
|
* 로그인 여부만 확인하는 간단한 가드
|
|
*/
|
|
export function RequireAuth({ children }: { children: ReactNode }) {
|
|
return <AuthGuard requireAuth={true}>{children}</AuthGuard>;
|
|
}
|
|
|
|
/**
|
|
* 관리자 권한을 요구하는 가드
|
|
*/
|
|
export function RequireAdmin({ children }: { children: ReactNode }) {
|
|
return (
|
|
<AuthGuard requireAuth={true} requireAdmin={true}>
|
|
{children}
|
|
</AuthGuard>
|
|
);
|
|
}
|
|
|
|
export default AuthGuard;
|