Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b204c14e95 |
@@ -30,3 +30,4 @@ settings.json
|
|||||||
|
|
||||||
apps/**/tsconfig.tsbuildinfo
|
apps/**/tsconfig.tsbuildinfo
|
||||||
logs/
|
logs/
|
||||||
|
apps/st/start_cs.log
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
AUTH_USER_ID,
|
AUTH_USER_ID,
|
||||||
CLAIM_NAME,
|
CLAIM_NAME,
|
||||||
REFRESH_TOKEN,
|
REFRESH_TOKEN,
|
||||||
|
DEVICE_ID,
|
||||||
} from "@/shared/config";
|
} from "@/shared/config";
|
||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
@@ -13,5 +14,6 @@ export async function POST() {
|
|||||||
cookieStore.delete(REFRESH_TOKEN);
|
cookieStore.delete(REFRESH_TOKEN);
|
||||||
cookieStore.delete(AUTH_USER_ID);
|
cookieStore.delete(AUTH_USER_ID);
|
||||||
cookieStore.delete(CLAIM_NAME);
|
cookieStore.delete(CLAIM_NAME);
|
||||||
|
cookieStore.delete(DEVICE_ID); // 로그아웃 시 deviceId 함께 삭제 → 재로그인 시 새 세션 생성
|
||||||
return NextResponse.json({ message: "Logged out" }, { status: 200 });
|
return NextResponse.json({ message: "Logged out" }, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
REFRESH_TOKEN,
|
REFRESH_TOKEN,
|
||||||
SERVER_API_URL,
|
SERVER_API_URL,
|
||||||
SITE_ID,
|
SITE_ID,
|
||||||
|
DEVICE_ID,
|
||||||
} from "@/shared/config";
|
} from "@/shared/config";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
@@ -20,9 +21,16 @@ async function handler(
|
|||||||
"X-Site-Id": SITE_ID,
|
"X-Site-Id": SITE_ID,
|
||||||
};
|
};
|
||||||
let body = {};
|
let body = {};
|
||||||
const isRefreshHandler = params.url.includes("refresh");
|
|
||||||
|
|
||||||
// silent refresh
|
const isRefreshHandler = params.url.includes("refresh");
|
||||||
|
const isLoginHandler = params.url.includes("login");
|
||||||
|
|
||||||
|
// ── Silent Refresh ──────────────────────────────────────────────────────────
|
||||||
|
// HttpOnly 쿠키에서 refresh-token 꺼내 Authorization 헤더에 주입.
|
||||||
|
// 쿠키에 저장된 device-id도 X-Device-Id 헤더로 함께 전달한다.
|
||||||
|
// 백엔드: TokenProvider.refreshToken(token, request, response)
|
||||||
|
// → userService.findByRefreshToken(refreshToken, deviceId) (Redis Hash 검증)
|
||||||
|
// → userService.updateRefreshToken(userUnqId, newToken, deviceId) (RTR 갱신)
|
||||||
if (isRefreshHandler) {
|
if (isRefreshHandler) {
|
||||||
const refreshToken = cookieStore.get(REFRESH_TOKEN);
|
const refreshToken = cookieStore.get(REFRESH_TOKEN);
|
||||||
headers[CLAIM_NAME] = refreshToken?.value ?? "";
|
headers[CLAIM_NAME] = refreshToken?.value ?? "";
|
||||||
@@ -30,6 +38,27 @@ async function handler(
|
|||||||
if (!headers[CLAIM_NAME]) {
|
if (!headers[CLAIM_NAME]) {
|
||||||
return NextResponse.json({ message: "Can't refresh" }, { status: 401 });
|
return NextResponse.json({ message: "Can't refresh" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// device-id 쿠키 → X-Device-Id 헤더 (Redis 다중 세션 식별)
|
||||||
|
const deviceId = cookieStore.get(DEVICE_ID);
|
||||||
|
if (deviceId?.value) {
|
||||||
|
headers["X-Device-Id"] = deviceId.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Login ────────────────────────────────────────────────────────────────────
|
||||||
|
// 클라이언트(login-service.ts)가 fetch 헤더에 X-Device-Id 를 포함해서 전송한다.
|
||||||
|
// BFF는 해당 헤더를 그대로 백엔드로 포워딩한다.
|
||||||
|
// 백엔드: AuthenticationFilter.successfulAuthentication
|
||||||
|
// → TokenProvider.createTokenAndAddHeader(request, ...)
|
||||||
|
// → request.getHeader("X-Device-Id") 로 deviceId 추출
|
||||||
|
// → userService.updateRefreshToken(userUnqId, refreshToken, deviceId)
|
||||||
|
// → Redis Hash[user:sessions:{userUnqId}][deviceId] = refreshToken (TTL 1일)
|
||||||
|
if (isLoginHandler) {
|
||||||
|
const deviceId = request.headers.get("X-Device-Id");
|
||||||
|
if (deviceId) {
|
||||||
|
headers["X-Device-Id"] = deviceId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -62,16 +91,28 @@ async function handler(
|
|||||||
|
|
||||||
const forwardedHost = request.headers.get("x-forwarded-host");
|
const forwardedHost = request.headers.get("x-forwarded-host");
|
||||||
const host = request.headers.get("host");
|
const host = request.headers.get("host");
|
||||||
const actualHost = forwardedHost || host || new URL(request.url).hostname.toString();
|
const rawHost =
|
||||||
|
forwardedHost || host || new URL(request.url).hostname.toString();
|
||||||
// 운영 환경에서 domain 설정
|
|
||||||
const domain = actualHost.includes("hasuon.or.kr") ? ".hasuon.or.kr" : actualHost;
|
// 포트 번호 제거 (예: "localhost:3000" → "localhost")
|
||||||
|
const hostname = rawHost.split(":")[0];
|
||||||
|
|
||||||
|
// 쿠키 domain 결정
|
||||||
|
// - localhost / 127.0.0.1: domain 미지정 (undefined) → 브라우저 기본 동작
|
||||||
|
// - 운영(hasuon.or.kr): 서브도메인 공유를 위해 최상위 도메인 앞에 점(.) 붙임
|
||||||
|
// - 그 외(IP, 기타 도메인): 현재 hostname 그대로 사용
|
||||||
|
const isLocal = hostname === "localhost" || hostname === "127.0.0.1";
|
||||||
|
const domain = isLocal
|
||||||
|
? undefined
|
||||||
|
: hostname.includes("hasuon.or.kr")
|
||||||
|
? ".hasuon.or.kr"
|
||||||
|
: hostname;
|
||||||
|
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
cookieStore.set(REFRESH_TOKEN, refreshToken, {
|
cookieStore.set(REFRESH_TOKEN, refreshToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
domain: domain,
|
...(domain !== undefined && { domain }),
|
||||||
path: "/",
|
path: "/",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,7 +132,6 @@ async function handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
props: { params: Promise<{ url: string[] }> },
|
props: { params: Promise<{ url: string[] }> },
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ACCESS_TOKEN,
|
ACCESS_TOKEN,
|
||||||
AUTH_USER_ID,
|
AUTH_USER_ID,
|
||||||
CLAIM_NAME,
|
CLAIM_NAME,
|
||||||
|
DEVICE_ID,
|
||||||
} from "@/shared/config";
|
} from "@/shared/config";
|
||||||
import type { LoginService, ILogin } from "../model/login-interface";
|
import type { LoginService, ILogin } from "../model/login-interface";
|
||||||
import { tokenManager } from "../lib/token-manager";
|
import { tokenManager } from "../lib/token-manager";
|
||||||
@@ -23,8 +24,21 @@ export class LoginApiService implements LoginService {
|
|||||||
|
|
||||||
public async login(payload: ILogin): Promise<void> {
|
public async login(payload: ILogin): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
// ✅ 로그인 요청 전에 deviceId 생성/결정
|
||||||
|
// - 같은 브라우저 재로그인 시: 기존 쿠키 재사용 → 동일 Redis 슬롯 유지
|
||||||
|
// - 최초 로그인 / 로그아웃 후 재로그인 시: 새 UUID 생성 → 새 Redis 슬롯
|
||||||
|
const existingDeviceId = tokenManager.getDeviceId();
|
||||||
|
const deviceId = existingDeviceId ?? crypto.randomUUID();
|
||||||
|
|
||||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
// ✅ 로그인 요청 헤더에 X-Device-Id 포함
|
||||||
|
// BFF route.ts → 백엔드 AuthenticationFilter → TokenProvider.createTokenAndAddHeader()
|
||||||
|
// → userService.updateRefreshToken(userUnqId, refreshToken, deviceId)
|
||||||
|
// → Redis Hash[user:sessions:{userUnqId}][deviceId] = refreshToken (TTL 1일)
|
||||||
|
"X-Device-Id": deviceId,
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
userId: payload.userId,
|
userId: payload.userId,
|
||||||
password: payload.password,
|
password: payload.password,
|
||||||
@@ -32,24 +46,27 @@ export class LoginApiService implements LoginService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('data ', data)
|
console.log("data ", data);
|
||||||
const accessToken = data[ACCESS_TOKEN];
|
const accessToken = data[ACCESS_TOKEN];
|
||||||
const tokenId = data[AUTH_USER_ID];
|
const tokenId = data[AUTH_USER_ID];
|
||||||
|
|
||||||
if (!accessToken || !tokenId) {
|
if (!accessToken || !tokenId) {
|
||||||
if (data.message == 'Invalid Credentials' ) {
|
if (data.message == "Invalid Credentials") {
|
||||||
throw new Error('아이디와 비밀번호가 일치하지 않습니다.');
|
throw new Error("아이디와 비밀번호가 일치하지 않습니다.");
|
||||||
} else if (data.message == 'User not found') {
|
} else if (data.message == "User not found") {
|
||||||
throw new Error('존재하지 않는 아이디입니다.');
|
throw new Error("존재하지 않는 아이디입니다.");
|
||||||
} else if (data.message == 'User is not active') {
|
} else if (data.message == "User is not active") {
|
||||||
throw new Error('활성화되지 않은 계정입니다.');
|
throw new Error("활성화되지 않은 계정입니다.");
|
||||||
} else if (data.message == 'User is not verified') {
|
} else if (data.message == "User is not verified") {
|
||||||
throw new Error('이메일 인증이 필요합니다.');
|
throw new Error("이메일 인증이 필요합니다.");
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.message);
|
throw new Error(data.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ 로그인 성공 후 deviceId 쿠키 저장 (maxAge=86400, Refresh Token TTL과 동기화)
|
||||||
|
tokenManager.setDeviceId(deviceId);
|
||||||
|
|
||||||
tokenManager.setToken(CLAIM_NAME, accessToken);
|
tokenManager.setToken(CLAIM_NAME, accessToken);
|
||||||
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
||||||
tokenManager.setupRefreshTimer(
|
tokenManager.setupRefreshTimer(
|
||||||
@@ -66,6 +83,10 @@ export class LoginApiService implements LoginService {
|
|||||||
|
|
||||||
public async silentRefresh(): Promise<void> {
|
public async silentRefresh(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
// deviceId는 BFF(route.ts)가 쿠키(device-id)에서 읽어 X-Device-Id 헤더로 자동 전달
|
||||||
|
// → 백엔드 TokenProvider.refreshToken(token, request, response)
|
||||||
|
// → userService.findByRefreshToken(refreshToken, deviceId) → Redis Hash 검증
|
||||||
|
// → userService.updateRefreshToken(userUnqId, newRefreshToken, deviceId) → RTR 갱신
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||||
{ method: "PUT" },
|
{ method: "PUT" },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
|
import { AUTH_USER_ID, CLAIM_NAME, DEVICE_ID } from "@/shared/config";
|
||||||
import { getCookie, setCookie, removeCookie } from "@/shared/lib";
|
import { getCookie, setCookie, removeCookie } from "@/shared/lib";
|
||||||
|
|
||||||
export class TokenManager {
|
export class TokenManager {
|
||||||
@@ -20,22 +20,18 @@ export class TokenManager {
|
|||||||
|
|
||||||
public setToken(key: string, value: string): void {
|
public setToken(key: string, value: string): void {
|
||||||
const domain = this.getCookieDomain();
|
const domain = this.getCookieDomain();
|
||||||
|
|
||||||
setCookie(key, value, {
|
setCookie(key, value, {
|
||||||
secure: window.location.protocol === "https:",
|
secure: window.location.protocol === "https:",
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
domain: domain,
|
domain: domain,
|
||||||
path: "/", // 모든 경로에서 접근 가능
|
path: "/",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private getCookieDomain(): string | undefined {
|
private getCookieDomain(): string | undefined {
|
||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
|
|
||||||
// 개발 환경에서는 localhost 도메인 사용
|
|
||||||
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
||||||
return undefined; // localhost에서는 도메인 설정하지 않음
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
|
// 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
|
||||||
@@ -52,6 +48,33 @@ export class TokenManager {
|
|||||||
removeCookie(key, { domain, path: "/" });
|
removeCookie(key, { domain, path: "/" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* deviceId 쿠키 저장
|
||||||
|
* - httpOnly가 아닌 일반 쿠키로 저장 (BFF route.ts에서 cookieStore.get()으로 읽기 위함)
|
||||||
|
* - js-cookie는 maxAge를 지원하지 않으므로 expires(Date)로 변환하여 사용
|
||||||
|
* - expires: Refresh Token TTL(1일)과 동일하게 설정하여 브라우저 재시작 후에도 동일 Redis 슬롯 재사용
|
||||||
|
*/
|
||||||
|
public setDeviceId(deviceId: string): void {
|
||||||
|
const domain = this.getCookieDomain();
|
||||||
|
// js-cookie v3는 maxAge 속성을 지원하지 않음 (number 타입에 .split() 호출로 TypeError 발생)
|
||||||
|
// maxAge(초) → expires(Date) 변환: 1일 = 86400초
|
||||||
|
const expires = new Date(Date.now() + 86400 * 1000);
|
||||||
|
setCookie(DEVICE_ID, deviceId, {
|
||||||
|
secure: window.location.protocol === "https:",
|
||||||
|
sameSite: "lax",
|
||||||
|
domain: domain,
|
||||||
|
path: "/",
|
||||||
|
expires, // Date 객체로 전달 (js-cookie가 toUTCString()으로 직렬화)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* deviceId 쿠키 조회
|
||||||
|
*/
|
||||||
|
public getDeviceId(): string | undefined {
|
||||||
|
return getCookie(DEVICE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
public setupRefreshTimer(
|
public setupRefreshTimer(
|
||||||
callback: () => Promise<void>,
|
callback: () => Promise<void>,
|
||||||
expirationTime: number,
|
expirationTime: number,
|
||||||
@@ -59,7 +82,6 @@ export class TokenManager {
|
|||||||
if (this.refreshTimer) {
|
if (this.refreshTimer) {
|
||||||
clearTimeout(this.refreshTimer);
|
clearTimeout(this.refreshTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshTimer = setTimeout(() => {
|
this.refreshTimer = setTimeout(() => {
|
||||||
callback().catch((error) => {
|
callback().catch((error) => {
|
||||||
console.error("Silent refresh failed:", error);
|
console.error("Silent refresh failed:", error);
|
||||||
@@ -73,9 +95,9 @@ export class TokenManager {
|
|||||||
clearTimeout(this.refreshTimer);
|
clearTimeout(this.refreshTimer);
|
||||||
this.refreshTimer = null;
|
this.refreshTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeToken(CLAIM_NAME);
|
this.removeToken(CLAIM_NAME);
|
||||||
this.removeToken(AUTH_USER_ID);
|
this.removeToken(AUTH_USER_ID);
|
||||||
|
this.removeToken(DEVICE_ID); // deviceId 함께 삭제
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ export const ACCESS_TOKEN =
|
|||||||
export const REFRESH_TOKEN =
|
export const REFRESH_TOKEN =
|
||||||
process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
|
process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
|
||||||
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
||||||
|
export const DEVICE_ID = process.env.NEXT_PUBLIC_DEVICE_ID || "device-id";
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import {
|
import {
|
||||||
ACCESS_TOKEN,
|
ACCESS_TOKEN,
|
||||||
AUTH_USER_ID,
|
AUTH_USER_ID,
|
||||||
CLAIM_NAME,
|
CLAIM_NAME,
|
||||||
REFRESH_TOKEN,
|
DEVICE_ID,
|
||||||
|
REFRESH_TOKEN,
|
||||||
} from "@/shared/config";
|
} from "@/shared/config";
|
||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
cookieStore.delete(ACCESS_TOKEN);
|
cookieStore.delete(ACCESS_TOKEN);
|
||||||
cookieStore.delete(REFRESH_TOKEN);
|
cookieStore.delete(REFRESH_TOKEN);
|
||||||
cookieStore.delete(AUTH_USER_ID);
|
cookieStore.delete(AUTH_USER_ID);
|
||||||
cookieStore.delete(CLAIM_NAME);
|
cookieStore.delete(CLAIM_NAME);
|
||||||
return NextResponse.json({ message: "Logged out" }, { status: 200 });
|
cookieStore.delete(DEVICE_ID);
|
||||||
|
return NextResponse.json({ message: "Logged out" }, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,158 @@
|
|||||||
import {
|
import {
|
||||||
ACCESS_TOKEN,
|
ACCESS_TOKEN,
|
||||||
AUTH_USER_ID,
|
AUTH_USER_ID,
|
||||||
CLAIM_NAME,
|
CLAIM_NAME,
|
||||||
REFRESH_TOKEN,
|
DEVICE_ID,
|
||||||
SERVER_API_URL,
|
REFRESH_TOKEN,
|
||||||
SITE_ID,
|
SERVER_API_URL,
|
||||||
|
SITE_ID,
|
||||||
} from "@/shared/config";
|
} from "@/shared/config";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
async function handler(
|
async function handler(
|
||||||
request: Request,
|
request: Request,
|
||||||
params: { url: string[] },
|
params: { url: string[] },
|
||||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||||
) {
|
) {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Site-Id": SITE_ID,
|
"X-Site-Id": SITE_ID,
|
||||||
};
|
};
|
||||||
let body = {};
|
let body = {};
|
||||||
const isRefreshHandler = params.url.includes("refresh");
|
|
||||||
|
|
||||||
// silent refresh
|
const isRefreshHandler = params.url.includes("refresh");
|
||||||
if (isRefreshHandler) {
|
const isLoginHandler = params.url.includes("login");
|
||||||
const refreshToken = cookieStore.get(REFRESH_TOKEN);
|
|
||||||
headers[CLAIM_NAME] = refreshToken?.value ?? "";
|
|
||||||
|
|
||||||
if (!headers[CLAIM_NAME]) {
|
// ── Silent Refresh ──────────────────────────────────────────────────────────
|
||||||
return NextResponse.json({ message: "Can't refresh" }, { status: 401 });
|
// HttpOnly 쿠키에서 refresh-token 꺼내 Authorization 헤더에 주입.
|
||||||
}
|
// 쿠키에 저장된 device-id도 X-Device-Id 헤더로 함께 전달한다.
|
||||||
}
|
// 백엔드: TokenProvider.refreshToken(token, request, response)
|
||||||
|
// → userService.findByRefreshToken(refreshToken, deviceId) (Redis Hash 검증)
|
||||||
|
// → userService.updateRefreshToken(userUnqId, newToken, deviceId) (RTR 갱신)
|
||||||
|
if (isRefreshHandler) {
|
||||||
|
const refreshToken = cookieStore.get(REFRESH_TOKEN);
|
||||||
|
headers[CLAIM_NAME] = refreshToken?.value ?? "";
|
||||||
|
|
||||||
try {
|
if (!headers[CLAIM_NAME]) {
|
||||||
if (!isRefreshHandler) {
|
return NextResponse.json(
|
||||||
body = await request.json();
|
{ message: "Can't refresh" },
|
||||||
|
{ status: 401 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// device-id 쿠키 → X-Device-Id 헤더 (Redis 다중 세션 식별)
|
||||||
|
const deviceId = cookieStore.get(DEVICE_ID);
|
||||||
|
if (deviceId?.value) {
|
||||||
|
headers["X-Device-Id"] = deviceId.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, {
|
// ── Login ────────────────────────────────────────────────────────────────────
|
||||||
method,
|
// 클라이언트(login-service.ts)가 fetch 헤더에 X-Device-Id 를 포함해서 전송한다.
|
||||||
headers,
|
// BFF는 해당 헤더를 그대로 백엔드로 포워딩한다.
|
||||||
body: JSON.stringify(body),
|
// 백엔드: AuthenticationFilter.successfulAuthentication
|
||||||
});
|
// → TokenProvider.createTokenAndAddHeader(request, ...)
|
||||||
|
// → request.getHeader("X-Device-Id") 로 deviceId 추출
|
||||||
if (!result.ok) {
|
// → userService.updateRefreshToken(userUnqId, refreshToken, deviceId)
|
||||||
return NextResponse.json(
|
// → Redis Hash[user:sessions:{userUnqId}][deviceId] = refreshToken (TTL 1일)
|
||||||
{ message: "Invalid Credentials" },
|
if (isLoginHandler) {
|
||||||
{ status: 401 },
|
const deviceId = request.headers.get("X-Device-Id");
|
||||||
);
|
if (deviceId) {
|
||||||
|
headers["X-Device-Id"] = deviceId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = result.headers.get(ACCESS_TOKEN);
|
try {
|
||||||
const refreshToken = result.headers.get(REFRESH_TOKEN);
|
if (!isRefreshHandler) {
|
||||||
const userId = result.headers.get(AUTH_USER_ID);
|
body = await request.json();
|
||||||
|
}
|
||||||
|
|
||||||
if (accessToken && userId) {
|
const result = await fetch(
|
||||||
const response = NextResponse.json({
|
`${SERVER_API_URL}/${params.url.join("/")}`,
|
||||||
[ACCESS_TOKEN]: accessToken,
|
{
|
||||||
[AUTH_USER_ID]: userId,
|
method,
|
||||||
});
|
headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Invalid Credentials" },
|
||||||
|
{ status: 401 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (refreshToken) {
|
const accessToken = result.headers.get(ACCESS_TOKEN);
|
||||||
cookieStore.set(REFRESH_TOKEN, refreshToken, {
|
const refreshToken = result.headers.get(REFRESH_TOKEN);
|
||||||
httpOnly: true,
|
const userId = result.headers.get(AUTH_USER_ID);
|
||||||
sameSite: "lax",
|
|
||||||
// domain: domain,
|
|
||||||
path: "/",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
if (accessToken && userId) {
|
||||||
|
const response = NextResponse.json({
|
||||||
|
[ACCESS_TOKEN]: accessToken,
|
||||||
|
[AUTH_USER_ID]: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const forwardedHost = request.headers.get("x-forwarded-host");
|
||||||
|
const host = request.headers.get("host");
|
||||||
|
const rawHost =
|
||||||
|
forwardedHost ||
|
||||||
|
host ||
|
||||||
|
new URL(request.url).hostname.toString();
|
||||||
|
|
||||||
|
// 포트 번호 제거 (예: "localhost:13000" → "localhost")
|
||||||
|
const hostname = rawHost.split(":")[0];
|
||||||
|
|
||||||
|
// 쿠키 domain 결정
|
||||||
|
// - localhost / 127.0.0.1: domain 미지정 (undefined) → 브라우저 기본 동작
|
||||||
|
// - 운영(hasuon.or.kr): 서브도메인 공유를 위해 최상위 도메인 앞에 점(.) 붙임
|
||||||
|
// - 그 외(IP, 기타 도메인): 현재 hostname 그대로 사용
|
||||||
|
const isLocal =
|
||||||
|
hostname === "localhost" || hostname === "127.0.0.1";
|
||||||
|
const domain = isLocal
|
||||||
|
? undefined
|
||||||
|
: hostname.includes("hasuon.or.kr")
|
||||||
|
? ".hasuon.or.kr"
|
||||||
|
: hostname;
|
||||||
|
|
||||||
|
if (refreshToken) {
|
||||||
|
cookieStore.set(REFRESH_TOKEN, refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "strict",
|
||||||
|
...(domain !== undefined && { domain }),
|
||||||
|
path: "/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Invalid Credentials" },
|
||||||
|
{ status: 401 },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Server Error", error },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: "Invalid Credentials" },
|
|
||||||
{ status: 401 },
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: "Server Error", error },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
props: { params: Promise<{ url: string[] }> },
|
props: { params: Promise<{ url: string[] }> },
|
||||||
) {
|
) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
return handler(request, params, "POST");
|
return handler(request, params, "POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
request: Request,
|
request: Request,
|
||||||
props: { params: Promise<{ url: string[] }> },
|
props: { params: Promise<{ url: string[] }> },
|
||||||
) {
|
) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
return handler(request, params, "PUT");
|
return handler(request, params, "PUT");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,95 +1,120 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ACCESS_TOKEN,
|
ACCESS_TOKEN,
|
||||||
AUTH_USER_ID,
|
AUTH_USER_ID,
|
||||||
CLAIM_NAME,
|
CLAIM_NAME,
|
||||||
ENDPOINT,
|
DEVICE_ID,
|
||||||
JWT_EXPIRATION_TIME,
|
ENDPOINT,
|
||||||
|
JWT_EXPIRATION_TIME,
|
||||||
} from "@/shared/config";
|
} from "@/shared/config";
|
||||||
import { getCookie } from "@/shared/lib";
|
|
||||||
|
|
||||||
import { tokenManager } from "../lib/token-manager";
|
import { tokenManager } from "../lib/token-manager";
|
||||||
import type { ILogin,LoginService } from "../model/login-interface";
|
import type { ILogin, LoginService } from "../model/login-interface";
|
||||||
|
|
||||||
import { clearTabStorePersist } from "@/store/tab-store";
|
import { clearTabStorePersist } from "@/store/tab-store";
|
||||||
|
|
||||||
export class LoginApiService implements LoginService {
|
export class LoginApiService implements LoginService {
|
||||||
private static instance: LoginApiService;
|
private static instance: LoginApiService;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
public static getInstance(): LoginApiService {
|
public static getInstance(): LoginApiService {
|
||||||
if (!LoginApiService.instance) {
|
if (!LoginApiService.instance) {
|
||||||
LoginApiService.instance = new LoginApiService();
|
LoginApiService.instance = new LoginApiService();
|
||||||
|
}
|
||||||
|
return LoginApiService.instance;
|
||||||
}
|
}
|
||||||
return LoginApiService.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async login(payload: ILogin): Promise<void> {
|
public async login(payload: ILogin): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
// ✅ 로그인 요청 전에 deviceId 생성/결정
|
||||||
method: "POST",
|
// - 같은 브라우저 재로그인 시: 기존 쿠키 재사용 → 동일 Redis 슬롯 유지
|
||||||
body: JSON.stringify({
|
// - 최초 로그인 / 로그아웃 후 재로그인 시: 새 UUID 생성 → 새 Redis 슬롯
|
||||||
userId: payload.userId,
|
const existingDeviceId = tokenManager.getDeviceId();
|
||||||
password: payload.password,
|
const deviceId = existingDeviceId ?? crypto.randomUUID();
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
const response = await fetch(
|
||||||
const accessToken = data[ACCESS_TOKEN];
|
`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`,
|
||||||
const tokenId = data[AUTH_USER_ID];
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
// ✅ 로그인 요청 헤더에 X-Device-Id 포함
|
||||||
|
// BFF route.ts → 백엔드 AuthenticationFilter → TokenProvider.createTokenAndAddHeader()
|
||||||
|
// → userService.updateRefreshToken(userUnqId, refreshToken, deviceId)
|
||||||
|
// → Redis Hash[user:sessions:{userUnqId}][deviceId] = refreshToken (TTL 1일)
|
||||||
|
"X-Device-Id": deviceId,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
userId: payload.userId,
|
||||||
|
password: payload.password,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!accessToken || !tokenId) {
|
const data = await response.json();
|
||||||
throw new Error("Missing required authentication tokens");
|
const accessToken = data[ACCESS_TOKEN];
|
||||||
}
|
const tokenId = data[AUTH_USER_ID];
|
||||||
|
|
||||||
const prevTokenId = getCookie(AUTH_USER_ID);
|
if (!accessToken || !tokenId) {
|
||||||
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
throw new Error("Missing required authentication tokens");
|
||||||
clearTabStorePersist();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tokenManager.setToken(CLAIM_NAME, accessToken);
|
const prevTokenId = tokenManager.getToken(AUTH_USER_ID);
|
||||||
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
||||||
tokenManager.setupRefreshTimer(
|
clearTabStorePersist();
|
||||||
() => this.silentRefresh(),
|
}
|
||||||
JWT_EXPIRATION_TIME,
|
|
||||||
);
|
// ✅ 로그인 성공 후 deviceId 쿠키 저장 (maxAge=86400, Refresh Token TTL과 동기화)
|
||||||
} catch (error) {
|
tokenManager.setDeviceId(deviceId);
|
||||||
if (error instanceof AxiosError) {
|
|
||||||
throw new Error(`Login failed: ${error.response?.data?.message}`);
|
tokenManager.setToken(CLAIM_NAME, accessToken);
|
||||||
}
|
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
||||||
throw new Error(`Login failed: ${error}`);
|
tokenManager.setupRefreshTimer(
|
||||||
|
() => this.silentRefresh(),
|
||||||
|
JWT_EXPIRATION_TIME,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
throw new Error(
|
||||||
|
`Login failed: ${error.response?.data?.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error(`Login failed: ${error}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public async silentRefresh(): Promise<void> {
|
public async silentRefresh(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
// deviceId는 BFF(route.ts)가 쿠키(device-id)에서 읽어 X-Device-Id 헤더로 자동 전달
|
||||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
// → 백엔드 TokenProvider.refreshToken(token, request, response)
|
||||||
{ method: "PUT" },
|
// → userService.findByRefreshToken(refreshToken, deviceId) → Redis Hash 검증
|
||||||
);
|
// → userService.updateRefreshToken(userUnqId, newRefreshToken, deviceId) → RTR 갱신
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||||
|
{ method: "PUT" },
|
||||||
|
);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const accessToken = data[ACCESS_TOKEN];
|
const accessToken = data[ACCESS_TOKEN];
|
||||||
const tokenId = data[AUTH_USER_ID];
|
const tokenId = data[AUTH_USER_ID];
|
||||||
|
|
||||||
if (!accessToken || !tokenId) {
|
if (!accessToken || !tokenId) {
|
||||||
throw new Error("Missing required authentication tokens");
|
throw new Error("Missing required authentication tokens");
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenManager.setToken(CLAIM_NAME, accessToken);
|
tokenManager.setToken(CLAIM_NAME, accessToken);
|
||||||
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
tokenManager.clearAuthToken();
|
tokenManager.clearAuthToken();
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Silent refresh failed: ${error.response?.data?.message}`,
|
`Silent refresh failed: ${error.response?.data?.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new Error(`Silent refresh failed: ${error}`);
|
throw new Error(`Silent refresh failed: ${error}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoginApi = LoginApiService.getInstance();
|
export const LoginApi = LoginApiService.getInstance();
|
||||||
|
|||||||
@@ -1,85 +1,113 @@
|
|||||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
|
import { AUTH_USER_ID, CLAIM_NAME, DEVICE_ID } from "@/shared/config";
|
||||||
import { getCookie, removeCookie,setCookie } from "@/shared/lib";
|
import { getCookie, removeCookie, setCookie } from "@/shared/lib";
|
||||||
|
|
||||||
import { clearTabStorePersist } from "@/store/tab-store";
|
import { clearTabStorePersist } from "@/store/tab-store";
|
||||||
|
|
||||||
export class TokenManager {
|
export class TokenManager {
|
||||||
private static instance: TokenManager;
|
private static instance: TokenManager;
|
||||||
private refreshTimer: NodeJS.Timeout | null = null;
|
private refreshTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
public static getInstance(): TokenManager {
|
public static getInstance(): TokenManager {
|
||||||
if (!TokenManager.instance) {
|
if (!TokenManager.instance) {
|
||||||
TokenManager.instance = new TokenManager();
|
TokenManager.instance = new TokenManager();
|
||||||
}
|
}
|
||||||
return TokenManager.instance;
|
return TokenManager.instance;
|
||||||
}
|
|
||||||
|
|
||||||
public getToken(key: string): string | undefined {
|
|
||||||
return getCookie(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setToken(key: string, value: string): void {
|
|
||||||
const domain = this.getCookieDomain();
|
|
||||||
|
|
||||||
setCookie(key, value, {
|
|
||||||
secure: window.location.protocol === "https:",
|
|
||||||
sameSite: "lax",
|
|
||||||
domain: domain,
|
|
||||||
path: "/", // 모든 경로에서 접근 가능
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private getCookieDomain(): string | undefined {
|
|
||||||
const hostname = window.location.hostname;
|
|
||||||
|
|
||||||
// 개발 환경에서는 localhost 도메인 사용
|
|
||||||
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
||||||
return undefined; // localhost에서는 도메인 설정하지 않음
|
|
||||||
}
|
|
||||||
|
|
||||||
// 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
|
|
||||||
if (hostname.includes("hasuon.or.kr")) {
|
|
||||||
return ".hasuon.or.kr";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기타 환경에서는 현재 호스트명 사용
|
|
||||||
return hostname;
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeToken(key: string): void {
|
|
||||||
const domain = this.getCookieDomain();
|
|
||||||
removeCookie(key, { domain, path: "/" });
|
|
||||||
}
|
|
||||||
|
|
||||||
public setupRefreshTimer(
|
|
||||||
callback: () => Promise<void>,
|
|
||||||
expirationTime: number,
|
|
||||||
): void {
|
|
||||||
if (this.refreshTimer) {
|
|
||||||
clearTimeout(this.refreshTimer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshTimer = setTimeout(() => {
|
public getToken(key: string): string | undefined {
|
||||||
callback().catch((error) => {
|
return getCookie(key);
|
||||||
console.error("Silent refresh failed:", error);
|
|
||||||
this.clearAuthToken();
|
|
||||||
});
|
|
||||||
}, expirationTime - 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearAuthToken(): void {
|
|
||||||
if (this.refreshTimer) {
|
|
||||||
clearTimeout(this.refreshTimer);
|
|
||||||
this.refreshTimer = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearTabStorePersist();
|
public setToken(key: string, value: string): void {
|
||||||
this.removeToken(CLAIM_NAME);
|
const domain = this.getCookieDomain();
|
||||||
this.removeToken(AUTH_USER_ID);
|
|
||||||
}
|
setCookie(key, value, {
|
||||||
|
secure: window.location.protocol === "https:",
|
||||||
|
sameSite: "lax",
|
||||||
|
domain: domain,
|
||||||
|
path: "/", // 모든 경로에서 접근 가능
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCookieDomain(): string | undefined {
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
|
||||||
|
// 개발 환경에서는 localhost 도메인 사용
|
||||||
|
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
||||||
|
return undefined; // localhost에서는 도메인 설정하지 않음
|
||||||
|
}
|
||||||
|
|
||||||
|
// 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
|
||||||
|
if (hostname.includes("hasuon.or.kr")) {
|
||||||
|
return ".hasuon.or.kr";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기타 환경에서는 현재 호스트명 사용
|
||||||
|
return hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeToken(key: string): void {
|
||||||
|
const domain = this.getCookieDomain();
|
||||||
|
removeCookie(key, { domain, path: "/" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* deviceId 쿠키 저장
|
||||||
|
* - httpOnly가 아닌 일반 쿠키로 저장 (BFF route.ts에서 cookieStore.get()으로 읽기 위함)
|
||||||
|
* - js-cookie v3는 maxAge 속성을 지원하지 않음
|
||||||
|
* (number 타입에 .split() 호출 → TypeError: attributes[attributeName].split is not a function)
|
||||||
|
* - maxAge(초) → expires(Date) 변환: 1일 = 86400초
|
||||||
|
* Refresh Token TTL(1일)과 동기화 → 브라우저 재시작 후에도 동일 Redis 슬롯 재사용
|
||||||
|
*/
|
||||||
|
public setDeviceId(deviceId: string): void {
|
||||||
|
const domain = this.getCookieDomain();
|
||||||
|
// js-cookie가 Date 객체를 받아 toUTCString()으로 직렬화함
|
||||||
|
const expires = new Date(Date.now() + 86400 * 1000);
|
||||||
|
setCookie(DEVICE_ID, deviceId, {
|
||||||
|
secure: window.location.protocol === "https:",
|
||||||
|
sameSite: "lax",
|
||||||
|
domain: domain,
|
||||||
|
path: "/",
|
||||||
|
expires, // maxAge 대신 expires(Date) 사용
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* deviceId 쿠키 조회
|
||||||
|
*/
|
||||||
|
public getDeviceId(): string | undefined {
|
||||||
|
return getCookie(DEVICE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTabStorePersist();
|
||||||
|
this.removeToken(CLAIM_NAME);
|
||||||
|
this.removeToken(AUTH_USER_ID);
|
||||||
|
this.removeToken(DEVICE_ID); // deviceId 함께 삭제
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tokenManager = TokenManager.getInstance();
|
export const tokenManager = TokenManager.getInstance();
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
||||||
export const PROXY_HOST =
|
export const PROXY_HOST =
|
||||||
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "ST";
|
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "ST";
|
||||||
export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
||||||
export const ACCESS_TOKEN =
|
export const ACCESS_TOKEN =
|
||||||
process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
|
process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
|
||||||
export const REFRESH_TOKEN =
|
export const REFRESH_TOKEN =
|
||||||
process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
|
process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
|
||||||
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
||||||
export const ST_SERVICE_URL = process.env.NEXT_PUBLIC_ST_SERVICE_URL || "http://localhost:8080";
|
export const ST_SERVICE_URL =
|
||||||
|
process.env.NEXT_PUBLIC_ST_SERVICE_URL || "http://localhost:8080";
|
||||||
|
export const DEVICE_ID = process.env.NEXT_PUBLIC_DEVICE_ID || "device-id";
|
||||||
|
|||||||
Reference in New Issue
Block a user