189 lines
5.3 KiB
TypeScript
189 lines
5.3 KiB
TypeScript
import { JWTPayload, UserInfo } from '@/types';
|
|
import { appBridge } from './appBridge';
|
|
import { jwtDecoder } from './jwtDecoder';
|
|
|
|
const ACCESS_TOKEN_KEY = 'nice_access_token';
|
|
const REFRESH_TOKEN_KEY = 'nice_refresh_token';
|
|
|
|
export class TokenManager {
|
|
private static instance: TokenManager;
|
|
|
|
private constructor() {}
|
|
|
|
static getInstance(): TokenManager {
|
|
if (!TokenManager.instance) {
|
|
TokenManager.instance = new TokenManager();
|
|
}
|
|
return TokenManager.instance;
|
|
}
|
|
|
|
setTokens(accessToken: string, refreshToken: string): void {
|
|
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
|
|
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
|
this.saveTokensToAppBridge(accessToken, refreshToken);
|
|
}
|
|
|
|
getAccessToken(): string | null {
|
|
console.log('getAccessToken')
|
|
return localStorage.getItem(ACCESS_TOKEN_KEY);
|
|
}
|
|
|
|
getRefreshToken(): string | null {
|
|
return localStorage.getItem(REFRESH_TOKEN_KEY);
|
|
}
|
|
|
|
clearTokens(): void {
|
|
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
|
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
|
}
|
|
|
|
isTokenExpired(token: string): boolean {
|
|
return jwtDecoder.isExpired(token);
|
|
}
|
|
|
|
isAccessTokenExpired(): boolean {
|
|
const token = this.getAccessToken();
|
|
if (!token) return true;
|
|
return this.isTokenExpired(token);
|
|
}
|
|
|
|
isRefreshTokenExpired(): boolean {
|
|
const token = this.getRefreshToken();
|
|
if (!token) return true;
|
|
return this.isTokenExpired(token);
|
|
}
|
|
|
|
decodeToken(token: string): JWTPayload | null {
|
|
return jwtDecoder.decode(token);
|
|
}
|
|
|
|
getUserInfoFromToken(token?: string): UserInfo | null {
|
|
const accessToken = token || this.getAccessToken();
|
|
if (!accessToken) return null;
|
|
|
|
const decoded = this.decodeToken(accessToken);
|
|
if (!decoded) return null;
|
|
|
|
return {
|
|
id: decoded.userId,
|
|
email: decoded.email,
|
|
name: '', // 토큰에서 이름을 가져오지 않으므로 빈 문자열
|
|
role: decoded.role,
|
|
createdAt: '',
|
|
updatedAt: ''
|
|
};
|
|
}
|
|
|
|
getTokenExpirationTime(token: string): number | null {
|
|
return jwtDecoder.getExpiration(token);
|
|
}
|
|
|
|
getTimeUntilExpiration(token: string): number {
|
|
return jwtDecoder.getTimeUntilExpiration(token);
|
|
}
|
|
|
|
shouldRefreshToken(): boolean {
|
|
const accessToken = this.getAccessToken();
|
|
if (!accessToken) return false;
|
|
|
|
const timeUntilExpiration = this.getTimeUntilExpiration(accessToken);
|
|
// 토큰이 5분 이내에 만료되면 갱신
|
|
const REFRESH_BUFFER_SECONDS = 5 * 60; // 5 minutes
|
|
return timeUntilExpiration < REFRESH_BUFFER_SECONDS;
|
|
}
|
|
|
|
hasValidTokens(): boolean {
|
|
const accessToken = this.getAccessToken();
|
|
const refreshToken = this.getRefreshToken();
|
|
|
|
if (!accessToken || !refreshToken) return false;
|
|
|
|
// 리프레시 토큰이 유효하면 액세스 토큰은 갱신 가능
|
|
return !this.isRefreshTokenExpired();
|
|
}
|
|
|
|
async refreshTokens(): Promise<{accessToken: string, refreshToken: string}> {
|
|
const refreshToken = this.getRefreshToken();
|
|
if (!refreshToken) {
|
|
throw new Error('No refresh token available');
|
|
}
|
|
|
|
const response = await fetch('http://3.35.79.250:8090/auth/v1/refresh', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ refreshToken }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Token refresh failed: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const newAccessToken = data.accessToken;
|
|
const newRefreshToken = data.refreshToken;
|
|
|
|
this.setTokens(newAccessToken, newRefreshToken);
|
|
|
|
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
|
|
}
|
|
|
|
async makeAuthenticatedRequest(url: string, options: RequestInit = {}): Promise<Response> {
|
|
let accessToken = this.getAccessToken();
|
|
|
|
if (!accessToken || this.isAccessTokenExpired()) {
|
|
try {
|
|
const refreshed = await this.refreshTokens();
|
|
accessToken = refreshed.accessToken;
|
|
} catch (error) {
|
|
this.clearTokens();
|
|
throw new Error('Authentication failed - please log in again');
|
|
}
|
|
}
|
|
|
|
const requestOptions = {
|
|
...options,
|
|
headers: {
|
|
...options.headers,
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
},
|
|
};
|
|
|
|
const response = await fetch(url, requestOptions);
|
|
|
|
if (response.status === 401) {
|
|
try {
|
|
const refreshed = await this.refreshTokens();
|
|
const retryOptions = {
|
|
...requestOptions,
|
|
headers: {
|
|
...requestOptions.headers,
|
|
'Authorization': `Bearer ${refreshed.accessToken}`,
|
|
},
|
|
};
|
|
return await fetch(url, retryOptions);
|
|
} catch (error) {
|
|
this.clearTokens();
|
|
throw new Error('Authentication failed - please log in again');
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
private async saveTokensToAppBridge(accessToken: string, refreshToken: string): Promise<void> {
|
|
try {
|
|
if (appBridge.isNativeEnvironment()) {
|
|
await Promise.all([
|
|
appBridge.setStorage(ACCESS_TOKEN_KEY, accessToken),
|
|
appBridge.setStorage(REFRESH_TOKEN_KEY, refreshToken)
|
|
]);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to save tokens to AppBridge:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const tokenManager = TokenManager.getInstance(); |