feat: 로그인 로직 구현
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"js-cookie": "^3.0.5",
|
||||
"next": "15.1.7",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
@@ -21,6 +22,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@faker-js/faker": "^9.5.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { AxiosError } from "axios";
|
||||
import { client } from "@/shared/api";
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
CLAIM_NAME,
|
||||
ENDPOINT,
|
||||
JWT_EXPIRATION_TIME,
|
||||
REFRESH_TOKEN,
|
||||
TOKEN_ID,
|
||||
} from "@/shared/config";
|
||||
import { tokenManager } from "../lib/token-manager";
|
||||
import type { LoginService, ILogin } from "../model/login";
|
||||
|
||||
export class LoginApiService implements LoginService {
|
||||
private static instance: LoginApiService;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): LoginApiService {
|
||||
if (!LoginApiService.instance) {
|
||||
LoginApiService.instance = new LoginApiService();
|
||||
}
|
||||
return LoginApiService.instance;
|
||||
}
|
||||
|
||||
public async login(payload: ILogin): Promise<void> {
|
||||
try {
|
||||
const response = await client.post(ENDPOINT.USER_SERVICE.LOGIN, {
|
||||
userId: payload.userId,
|
||||
password: payload.password,
|
||||
});
|
||||
|
||||
const accessToken = response.headers[ACCESS_TOKEN];
|
||||
const refreshToken = response.headers[REFRESH_TOKEN];
|
||||
const tokenId = response.headers[TOKEN_ID];
|
||||
|
||||
if (!refreshToken || !accessToken || !tokenId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
tokenManager.setToken(ACCESS_TOKEN, accessToken);
|
||||
tokenManager.setToken(TOKEN_ID, tokenId);
|
||||
tokenManager.setRefreshToken(refreshToken);
|
||||
tokenManager.setupRefreshTimer(
|
||||
() => this.silentRefresh(),
|
||||
JWT_EXPIRATION_TIME,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new Error(
|
||||
`Login failed: ${error.response?.data?.message || error.message}`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async silentRefresh(): Promise<void> {
|
||||
try {
|
||||
const refreshToken = tokenManager.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
throw new Error("No refresh token available");
|
||||
}
|
||||
|
||||
const response = await client.put(
|
||||
ENDPOINT.USER_SERVICE.SILENT_REFRESH,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
[CLAIM_NAME]: refreshToken,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const accessToken = response.headers[ACCESS_TOKEN];
|
||||
const tokenId = response.headers[TOKEN_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
tokenManager.setToken(ACCESS_TOKEN, accessToken);
|
||||
tokenManager.setToken(TOKEN_ID, tokenId);
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new Error(
|
||||
`Silent refresh failed: ${error.response?.data?.message || error.message}`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const LoginApi = LoginApiService.getInstance();
|
||||
@@ -0,0 +1,61 @@
|
||||
import axios from "axios";
|
||||
import Cookies from "js-cookie";
|
||||
import { CLAIM_NAME, REFRESH_TOKEN, TOKEN_ID } from "@/shared/config";
|
||||
|
||||
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 setToken(key: string, value: string): void {
|
||||
axios.defaults.headers.common[key] = value;
|
||||
}
|
||||
|
||||
public setRefreshToken(token: string): void {
|
||||
Cookies.set(REFRESH_TOKEN, token, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "lax",
|
||||
});
|
||||
}
|
||||
|
||||
public getRefreshToken(): string | undefined {
|
||||
return Cookies.get(REFRESH_TOKEN);
|
||||
}
|
||||
|
||||
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.clearAuth();
|
||||
});
|
||||
}, expirationTime - 60000);
|
||||
}
|
||||
|
||||
public clearAuth(): void {
|
||||
if (this.refreshTimer) {
|
||||
clearTimeout(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
Cookies.remove(REFRESH_TOKEN);
|
||||
delete axios.defaults.headers.common[CLAIM_NAME];
|
||||
delete axios.defaults.headers.common[TOKEN_ID];
|
||||
}
|
||||
}
|
||||
|
||||
export const tokenManager = TokenManager.getInstance();
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface ILogin {
|
||||
userId: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginService {
|
||||
login(payload: ILogin): Promise<void>;
|
||||
}
|
||||
@@ -1,15 +1,6 @@
|
||||
import axios from "axios";
|
||||
import type { AxiosInstance, InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
interface ApiResponse<T = unknown> {
|
||||
data: T;
|
||||
status: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface RequestData {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { SERVER_API_URL, SITE_ID } from "../config";
|
||||
|
||||
class Client {
|
||||
private static instance: Client;
|
||||
@@ -17,10 +8,12 @@ class Client {
|
||||
|
||||
private constructor() {
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||
baseURL: SERVER_API_URL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,49 +24,38 @@ class Client {
|
||||
return Client.instance;
|
||||
}
|
||||
|
||||
public async get<T>(
|
||||
public async get<T = unknown, R = AxiosResponse<T>>(
|
||||
url: string,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.get<ApiResponse<T>>(url, config);
|
||||
return response.data;
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<R> {
|
||||
const response = await this.axiosInstance.get<T, R>(url, config);
|
||||
return response;
|
||||
}
|
||||
|
||||
public async post<T>(
|
||||
public async post<T = unknown, R = AxiosResponse<T>, D = unknown>(
|
||||
url: string,
|
||||
data?: RequestData,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.post<ApiResponse<T>>(
|
||||
url,
|
||||
data,
|
||||
config,
|
||||
);
|
||||
return response.data;
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>,
|
||||
): Promise<R> {
|
||||
const response = await this.axiosInstance.post<T, R>(url, data, config);
|
||||
return response;
|
||||
}
|
||||
|
||||
public async put<T>(
|
||||
public async put<T = unknown, R = AxiosResponse<T>, D = unknown>(
|
||||
url: string,
|
||||
data?: RequestData,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.put<ApiResponse<T>>(
|
||||
url,
|
||||
data,
|
||||
config,
|
||||
);
|
||||
return response.data;
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>,
|
||||
): Promise<R> {
|
||||
const response = await this.axiosInstance.put<T, R>(url, data, config);
|
||||
return response;
|
||||
}
|
||||
|
||||
public async delete<T>(
|
||||
public async delete<T = unknown, R = AxiosResponse<T>>(
|
||||
url: string,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const response = await this.axiosInstance.delete<ApiResponse<T>>(
|
||||
url,
|
||||
config,
|
||||
);
|
||||
return response.data;
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<R> {
|
||||
const response = await this.axiosInstance.delete<T, R>(url, config);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const ENDPOINT = {
|
||||
USER_SERVICE: {
|
||||
LOGIN: "/user-service/login",
|
||||
SILENT_REFRESH: "/user-service/api/v1/users/token/refresh",
|
||||
},
|
||||
};
|
||||
|
||||
export const JWT_EXPIRATION_TIME = 1800000;
|
||||
@@ -0,0 +1,7 @@
|
||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID;
|
||||
|
||||
export const CLAIM_NAME = process.env.CLAIM_NAME || "Authorization";
|
||||
export const ACCESS_TOKEN = process.env.ACCESS_TOKEN || "access-token";
|
||||
export const REFRESH_TOKEN = process.env.REFRESH_TOKEN || "refresh-token";
|
||||
export const TOKEN_ID = process.env.TOKEN_ID || "token-id";
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./api";
|
||||
export * from "./env";
|
||||
Reference in New Issue
Block a user