첫 커밋
This commit is contained in:
188
src/utils/tokenManager.ts
Normal file
188
src/utils/tokenManager.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
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 {
|
||||
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();
|
||||
Reference in New Issue
Block a user