[common] feat: 공통 auth 및 components 수정(authService DI 패턴)
This commit is contained in:
@@ -4,4 +4,9 @@ export type { TokenManager } from "./token-manager";
|
||||
export { getCookie, setCookie, removeCookie } from "./cookie";
|
||||
export { getJwtExpiry, getRemainingMs } from "./jwt-utils";
|
||||
|
||||
export type { AuthConfig, CookieOptions } from "./types";
|
||||
export type {
|
||||
AuthConfig,
|
||||
AuthPayload,
|
||||
AuthService,
|
||||
CookieOptions,
|
||||
} from "./types";
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
export interface AuthPayload {
|
||||
userId: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface AuthService {
|
||||
login(payload: AuthPayload): Promise<void>;
|
||||
silentRefresh(): Promise<void>;
|
||||
logout(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface AuthConfig {
|
||||
authUserId: string;
|
||||
claimName: string;
|
||||
|
||||
@@ -3,7 +3,7 @@ import "./header.css";
|
||||
import { useEffect, useCallback, useState, useRef } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { images } from "@common/assets";
|
||||
import type { TokenManager } from "@common/auth";
|
||||
import type { AuthService, TokenManager } from "@common/auth";
|
||||
import { getConfig } from "@common/config";
|
||||
import { ENDPOINT } from "../../config";
|
||||
import { setCookie, getCookie } from "../../lib";
|
||||
@@ -29,7 +29,84 @@ const userId = getCookie(appConfig.AUTH_USER_ID ?? "");
|
||||
const siteId = getCookie("site-id-key") ?? "";
|
||||
const isLoggedIn = !!userId;
|
||||
|
||||
export function Header({ tokenManager, viewOnly }: { tokenManager?: TokenManager; viewOnly?: boolean }) {
|
||||
function getRootDomain(hostname: string): string {
|
||||
if (hostname === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
if (hostname.includes("hasuon.or.kr")) {
|
||||
return ".hasuon.or.kr";
|
||||
}
|
||||
|
||||
const parts = hostname.split(".");
|
||||
if (parts.length >= 2) {
|
||||
return `.${parts.slice(-2).join(".")}`;
|
||||
}
|
||||
|
||||
return hostname;
|
||||
}
|
||||
|
||||
function clearClientCookies(tokenManager?: TokenManager) {
|
||||
const actualHost = window.location.hostname;
|
||||
const domain = getRootDomain(actualHost);
|
||||
|
||||
const cookiesToDelete = [
|
||||
appConfig.AUTH_USER_ID,
|
||||
appConfig.REFRESH_TOKEN,
|
||||
appConfig.ACCESS_TOKEN,
|
||||
appConfig.CLAIM_NAME,
|
||||
"site-id-key",
|
||||
"site-theme-key",
|
||||
].filter(Boolean);
|
||||
|
||||
cookiesToDelete.forEach((cookieName) => {
|
||||
if (!cookieName) return;
|
||||
|
||||
tokenManager?.removeToken(cookieName);
|
||||
|
||||
if (domain !== "localhost") {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}`;
|
||||
}
|
||||
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||
|
||||
if (window.location.protocol === "https:") {
|
||||
if (domain !== "localhost") {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}; secure`;
|
||||
}
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; secure`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function legacyLogout(tokenManager?: TokenManager) {
|
||||
try {
|
||||
const response = await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Logout API failed");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Logout error:", error);
|
||||
} finally {
|
||||
clearClientCookies(tokenManager);
|
||||
window.location.href = "/auth/login";
|
||||
}
|
||||
}
|
||||
|
||||
export function Header({
|
||||
authService,
|
||||
tokenManager,
|
||||
viewOnly,
|
||||
}: {
|
||||
authService?: AuthService;
|
||||
/** @deprecated authService를 사용하세요. */
|
||||
tokenManager?: TokenManager;
|
||||
viewOnly?: boolean;
|
||||
}) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [logoWidth, setLogoWidth] = useState(0);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
@@ -66,93 +143,13 @@ export function Header({ tokenManager, viewOnly }: { tokenManager?: TokenManager
|
||||
queryFn: () => getSiteList(),
|
||||
enabled: isLoggedIn && !viewOnly,
|
||||
});
|
||||
const logout = async () => {
|
||||
try {
|
||||
// 1. 백엔드 API 로그아웃 호출
|
||||
const response = await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
credentials: "include", // ⭐ 쿠키 포함 중요!
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Logout API failed");
|
||||
// API 실패해도 계속 진행
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Logout error:", error);
|
||||
// 에러가 나도 계속 진행
|
||||
} finally {
|
||||
// 2. 클라이언트 쿠키 삭제 (백업)
|
||||
clearClientCookies();
|
||||
|
||||
// 3. 로컬/세션 스토리지 정리
|
||||
// if (typeof window !== "undefined") {
|
||||
// localStorage.clear();
|
||||
// sessionStorage.clear();
|
||||
// }
|
||||
|
||||
// 4. 로그인 페이지로 리다이렉트
|
||||
window.location.href = "/auth/login";
|
||||
const handleLogout = () => {
|
||||
if (authService) {
|
||||
void authService.logout();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 쿠키 삭제 헬퍼 함수
|
||||
const clearClientCookies = () => {
|
||||
const actualHost = window.location.hostname;
|
||||
|
||||
// 루트 도메인 추출
|
||||
const getRootDomain = (hostname: string): string => {
|
||||
if (hostname === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
if (hostname.includes("hasuon.or.kr")) {
|
||||
return ".hasuon.or.kr";
|
||||
}
|
||||
|
||||
const parts = hostname.split(".");
|
||||
if (parts.length >= 2) {
|
||||
return `.${parts.slice(-2).join(".")}`;
|
||||
}
|
||||
|
||||
return hostname;
|
||||
};
|
||||
|
||||
const domain = getRootDomain(actualHost);
|
||||
|
||||
// 삭제할 쿠키 목록
|
||||
const cookiesToDelete = [
|
||||
appConfig.AUTH_USER_ID,
|
||||
appConfig.REFRESH_TOKEN,
|
||||
appConfig.ACCESS_TOKEN,
|
||||
appConfig.CLAIM_NAME,
|
||||
"site-id-key",
|
||||
"site-theme-key",
|
||||
].filter(Boolean); // undefined 제거
|
||||
|
||||
// 여러 방법으로 쿠키 삭제 시도
|
||||
cookiesToDelete.forEach((cookieName) => {
|
||||
if (!cookieName) return;
|
||||
|
||||
// 방법 1: removeToken 함수 사용
|
||||
tokenManager?.removeToken(cookieName);
|
||||
|
||||
// 방법 2: 직접 document.cookie 설정 (도메인 포함)
|
||||
if (domain !== "localhost") {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}`;
|
||||
}
|
||||
|
||||
// 방법 3: 도메인 없이 삭제 (현재 도메인만)
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||
|
||||
// 방법 4: Secure 속성 포함 삭제
|
||||
if (window.location.protocol === "https:") {
|
||||
if (domain !== "localhost") {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}; secure`;
|
||||
}
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; secure`;
|
||||
}
|
||||
});
|
||||
void legacyLogout(tokenManager);
|
||||
};
|
||||
|
||||
const openPopup = useCallback(async (popup: Popup) => {
|
||||
@@ -270,7 +267,7 @@ export function Header({ tokenManager, viewOnly }: { tokenManager?: TokenManager
|
||||
<NoticePopover />
|
||||
</li>
|
||||
<li
|
||||
onClick={logout}
|
||||
onClick={handleLogout}
|
||||
className="cursor-pointer rounded-sm p-3 text-xl hover:bg-accent"
|
||||
>
|
||||
로그아웃
|
||||
@@ -346,7 +343,7 @@ export function Header({ tokenManager, viewOnly }: { tokenManager?: TokenManager
|
||||
<NoticePopover />
|
||||
</li>
|
||||
<li
|
||||
onClick={logout}
|
||||
onClick={handleLogout}
|
||||
className="cursor-pointer rounded-sm p-3 text-xl hover:bg-accent"
|
||||
>
|
||||
로그아웃
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TokenManager } from "@common/auth";
|
||||
import type { AuthService, TokenManager } from "@common/auth";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { QueryProvider } from "../../provider/query-provider";
|
||||
import { PopupProvider } from "../../provider/popup-provider";
|
||||
@@ -14,12 +14,15 @@ import "./common.css";
|
||||
export function BaseLayout({
|
||||
children,
|
||||
queryClient,
|
||||
authService,
|
||||
tokenManager,
|
||||
currentPath,
|
||||
handleMenuClick,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
queryClient?: QueryClient;
|
||||
authService?: AuthService;
|
||||
/** @deprecated authService를 사용하세요. authService가 없을 때만 Header legacy logout에 사용됩니다. */
|
||||
tokenManager?: TokenManager;
|
||||
currentPath?: string;
|
||||
handleMenuClick?: (item: MenuItem) => void;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AuthService, TokenManager } from "@common/auth";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { QueryProvider } from "../../provider/query-provider";
|
||||
import { PopupProvider } from "../../provider/popup-provider";
|
||||
@@ -13,11 +14,16 @@ import "./common.css";
|
||||
export function NoLNBLayout({
|
||||
children,
|
||||
queryClient,
|
||||
authService,
|
||||
tokenManager,
|
||||
currentPath,
|
||||
handleMenuClick,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
queryClient?: QueryClient;
|
||||
authService?: AuthService;
|
||||
/** @deprecated authService를 사용하세요. authService가 없을 때만 Header legacy logout에 사용됩니다. */
|
||||
tokenManager?: TokenManager;
|
||||
currentPath?: string;
|
||||
handleMenuClick?: (item: MenuItem) => void;
|
||||
}) {
|
||||
@@ -28,7 +34,10 @@ export function NoLNBLayout({
|
||||
<HelpDialogProvider>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
<Header
|
||||
authService={authService}
|
||||
tokenManager={authService ? undefined : tokenManager}
|
||||
/>
|
||||
<div className="flex">
|
||||
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
|
||||
<div className="flex-1">{children}</div>
|
||||
|
||||
Reference in New Issue
Block a user