110 lines
2.8 KiB
TypeScript
110 lines
2.8 KiB
TypeScript
import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config";
|
|
import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie";
|
|
|
|
type JwtPayload = {
|
|
sub?: string; // 토큰 subject
|
|
exp?: number; // 만료 시각(초 단위)
|
|
iat?: number; // 발급 시각(초 단위)
|
|
[key: string]: unknown;
|
|
};
|
|
|
|
export class TokenManager {
|
|
private static instance: TokenManager;
|
|
private refreshTimer: NodeJS.Timeout | null = null;
|
|
|
|
private constructor() {}
|
|
|
|
public static getInstance(): TokenManager {
|
|
if (!TokenManager.instance) {
|
|
TokenManager.instance = new TokenManager();
|
|
}
|
|
return TokenManager.instance;
|
|
}
|
|
|
|
public getToken(key: string): string | undefined {
|
|
return getCookie(key);
|
|
}
|
|
|
|
private parseJwt(token: string): JwtPayload {
|
|
const parts = token.split(".");
|
|
if (parts.length !== 3) {
|
|
throw new Error("유효한 JWT 형식이 아닙니다.");
|
|
}
|
|
|
|
const base64Url = parts[1] ?? ""; // Payload를 가져온다.
|
|
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
const jsonPayload = decodeURIComponent(
|
|
atob(padded)
|
|
.split("")
|
|
.map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`)
|
|
.join("")
|
|
);
|
|
|
|
return JSON.parse(jsonPayload) as JwtPayload;
|
|
}
|
|
|
|
public getRemainingMsFromToken(key: string = CLAIM_NAME): number | null {
|
|
const token = this.getToken(key);
|
|
if (!token) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const payload = this.parseJwt(token);
|
|
if (typeof payload.exp !== "number") {
|
|
return null;
|
|
}
|
|
|
|
return payload.exp * 1000 - Date.now();
|
|
} catch (error) {
|
|
console.error("JWT 파싱 오류:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public isExpiredToken(key: string = CLAIM_NAME): boolean {
|
|
const remainingMs = this.getRemainingMsFromToken(key);
|
|
return remainingMs === null || remainingMs <= 0;
|
|
}
|
|
|
|
public setToken(key: string, value: string): void {
|
|
setCookie(key, value, {
|
|
secure: window.location.protocol === "https:",
|
|
sameSite: "lax",
|
|
});
|
|
}
|
|
|
|
public removeToken(key: string): void {
|
|
removeCookie(key);
|
|
}
|
|
|
|
public setupRefreshTimer(
|
|
callback: () => Promise<void>,
|
|
expirationTime: number,
|
|
): void {
|
|
if (this.refreshTimer) {
|
|
clearTimeout(this.refreshTimer);
|
|
}
|
|
|
|
this.refreshTimer = setTimeout(() => {
|
|
callback().catch((error) => {
|
|
console.error("Silent refresh failed:", error);
|
|
this.clearAuthToken();
|
|
});
|
|
}, expirationTime - 60000);
|
|
}
|
|
|
|
public clearAuthToken(): void {
|
|
if (this.refreshTimer) {
|
|
clearTimeout(this.refreshTimer);
|
|
this.refreshTimer = null;
|
|
}
|
|
|
|
this.removeToken(CLAIM_NAME);
|
|
this.removeToken(AUTH_USER_ID);
|
|
}
|
|
}
|
|
|
|
export const tokenManager = TokenManager.getInstance();
|