Files
invyone/frontend/components/auth/AuthGuard.tsx
T
gbpark 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>
2026-04-25 00:36:05 +09:00

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;