From fb9f2a9d4a2b4e0afe5c1b240718c1c47bd25c07 Mon Sep 17 00:00:00 2001 From: YangJinSoo94 Date: Tue, 22 Jul 2025 15:03:24 +0900 Subject: [PATCH 1/2] =?UTF-8?q?middleware=20=EC=9E=91=EC=97=85=EC=A7=84?= =?UTF-8?q?=ED=96=89=EC=A4=91=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/dm/middleware.ts | 129 ++++++++++++++++++ .../dm/src/entities/auth/api/login-service.ts | 80 +++++++++++ apps/dm/src/entities/auth/index.ts | 2 + .../dm/src/entities/auth/lib/auth-wrapper.tsx | 45 ++++++ .../dm/src/entities/auth/lib/token-manager.ts | 59 ++++++++ .../entities/auth/model/login-interface.ts | 9 ++ apps/dm/src/shared/config/api.ts | 56 ++++++++ apps/dm/src/shared/config/env.ts | 13 ++ apps/dm/src/shared/config/index.ts | 2 + apps/dm/src/shared/lib/cookie.ts | 31 +++++ 10 files changed, 426 insertions(+) create mode 100644 apps/dm/middleware.ts create mode 100644 apps/dm/src/entities/auth/api/login-service.ts create mode 100644 apps/dm/src/entities/auth/index.ts create mode 100644 apps/dm/src/entities/auth/lib/auth-wrapper.tsx create mode 100644 apps/dm/src/entities/auth/lib/token-manager.ts create mode 100644 apps/dm/src/entities/auth/model/login-interface.ts create mode 100644 apps/dm/src/shared/config/api.ts create mode 100644 apps/dm/src/shared/config/env.ts create mode 100644 apps/dm/src/shared/config/index.ts create mode 100644 apps/dm/src/shared/lib/cookie.ts diff --git a/apps/dm/middleware.ts b/apps/dm/middleware.ts new file mode 100644 index 000000000..cb8365e4e --- /dev/null +++ b/apps/dm/middleware.ts @@ -0,0 +1,129 @@ +import { NextRequest, NextResponse } from "next/server"; +import { CS_URL } from "./components/ui/config"; +import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; + +// const CLAIM_NAME = "Authorization"; +// const AUTH_USER_ID = "token-id"; + const serverApiUrl = CS_URL; +const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; +const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || ""; +const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; + +export function middleware(req: NextRequest) { + const pathname = req.nextUrl.pathname; + console.log(`[Middleware] Request URL: ${req.url}`); + const searchParams = req.nextUrl.searchParams; + + // if (pathname.startsWith("/server")) { + // const queryParams = searchParams.toString(); + // const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ + // queryParams ? `?${queryParams}` : "" + // }`; + // console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); + // return NextResponse.rewrite(new URL(newUrl)); + // } + + if (pathname.startsWith("/server")) { + const decodedParams = Array.from(searchParams.entries()) + .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) + .join("&"); + + console.log(' sesesesesesevereveraserwereqwrewbrtkehjwrtg server') + const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ + decodedParams ? `?${decodedParams}` : "" + }`; + return NextResponse.rewrite(new URL(newUrl)); + } + + + if (pathname.startsWith("/attach-service")) { + const requestHeaders = new Headers(req.headers); + // const token = + // "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhMGVjN2EwNy03NTJhLTRmN2YtODlhNi04NTRlNjNkMmUzYTAiLCJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sUk9MRV9BTk9OWU1PVVMiLCJtbmdyWW4iOiJZIiwiZXhwIjoxNzUyMTM1OTk3fQ.itGY6M6mC2iwKvEpXw2KS6354T_B_OFz4hD26ResIgwJhpwiDJCMuSXD-rZ3AarmAIKd0udpCuJR7UlvFMN2ww"; + // const userId = "a0ec7a07-752a-4f7f-89a6-854e63d2e3a0"; + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; + const newUrl = `${serverApiUrl}${pathname}`; + + //wr:dev: pathname /attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad + //http://dev.onasys.co.kr:8000/attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad + if (token) requestHeaders.set(CLAIM_NAME, token); + if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + console.log( + `[Middleware] Rewriting from '/attach-service' path to: ${newUrl}` + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + + if (pathname.startsWith("/user-service")) { + const requestHeaders = new Headers(req.headers); + const newUrl = `${serverApiUrl}${pathname}`; + + console.log( + `[Middleware] Rewriting from '/user-service' path to: ${newUrl}` + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders + }); + } + + if (pathname.startsWith("/geoserver")) { + let queryParams = searchParams.toString(); + const typename = searchParams.get("TYPENAME"); + console.log("typename ", typename); + + let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + + if ( + typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설 + || typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역 + || typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도 + || typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도 + ) { + newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } + else if ( + typename == "hasudo:tb_ms_swage_ppl" //관로 + || typename == "hasudo:tb_ms_swage_mhl" //맨홀 + || typename == "hasudo:tb_ms_swage_pmstn" //펌프장 + || typename == "hasudo:tb_ms_stofcbr" //우수토실 + ) { + newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; + } + else if ( + typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실 + || typename == "hasudo:om_wqlt_station" //관측소 + || typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설 + || typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설 + || typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설 + || typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설 + || typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처 + || typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처 + ){ + const url = req.nextUrl.clone(); + url.searchParams.set('TYPENAME', typename.replace("hasudo:","")); + queryParams = url.searchParams.toString(); + newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`; + } + + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/sewerage-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/server/:path*", "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*"], +}; diff --git a/apps/dm/src/entities/auth/api/login-service.ts b/apps/dm/src/entities/auth/api/login-service.ts new file mode 100644 index 000000000..3cd387507 --- /dev/null +++ b/apps/dm/src/entities/auth/api/login-service.ts @@ -0,0 +1,80 @@ + +import type { LoginService, ILogin } from "../model/login-interface"; +import { tokenManager } from "../lib/token-manager"; +import {PROXY_HOST, ACCESS_TOKEN, AUTH_USER_ID,SITE_ID, CLAIM_NAME, ENDPOINT, JWT_EXPIRATION_TIME } from "../../../shared/config"; + +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 { + try { + console.log(' ENDPOINT.USER_SERVICE.LOGIN ', ENDPOINT.USER_SERVICE.LOGIN) + const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, { + method: "POST", + body: JSON.stringify({ + userId: payload.userId, + password: payload.password, + }), + }); + // const data = response.headers.get(ACCESS_TOKEN); + const accessToken = response.headers.get(ACCESS_TOKEN); + const tokenId = response.headers.get(AUTH_USER_ID); + + console.log('accessToken ', accessToken) + console.log('tokenId ', tokenId) + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + tokenManager.setToken("site-id-key", SITE_ID); + tokenManager.setupRefreshTimer( + () => this.silentRefresh(), + JWT_EXPIRATION_TIME, + ); + } catch (error) { + + console.log(' login err ' , error) + throw new Error(`Login failed: ${error}`); + } + } + + + public async silentRefresh(): Promise { + console.log(' silentRefresh ') + + try { + const response = await fetch(PROXY_HOST+ + `${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, + { method: "PUT" }, + ); + + const data = await response.json(); + const accessToken = data[ACCESS_TOKEN]; + const tokenId = data[AUTH_USER_ID]; + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + } catch (error) { + tokenManager.clearAuthToken(); + throw new Error(`Silent refresh failed: ${error}`); + } + } +} + +export const LoginApi = LoginApiService.getInstance(); diff --git a/apps/dm/src/entities/auth/index.ts b/apps/dm/src/entities/auth/index.ts new file mode 100644 index 000000000..9dc9dd368 --- /dev/null +++ b/apps/dm/src/entities/auth/index.ts @@ -0,0 +1,2 @@ +export { LoginApi } from "./api/login-service"; +//export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/dm/src/entities/auth/lib/auth-wrapper.tsx b/apps/dm/src/entities/auth/lib/auth-wrapper.tsx new file mode 100644 index 000000000..678ff50d5 --- /dev/null +++ b/apps/dm/src/entities/auth/lib/auth-wrapper.tsx @@ -0,0 +1,45 @@ +// "use client"; + +// import { useEffect } from "react"; +// import { useRouter } from "next/navigation"; +// import { LoginApi } from "../api/login-service"; + +// const AuthWrapper = ({ children }: { children: React.ReactNode }) => { +// const router = useRouter(); +// const { userInfo, setUserInfo } = useUserStore((state) => state); + +// async function authCheck() { +// const userId = getCookie(AUTH_USER_ID); +// if (!userId) { +// throw new Error("Missing required authentication tokens"); +// } + +// const response = await client.get( +// `${ENDPOINT.USER_SERVICE.USERS}/info/me`, +// ); +// if (!userInfo) setUserInfo(response.data); +// } + +// useEffect(() => { +// (async () => { +// try { +// await authCheck(); +// } catch (error) { +// console.error(error); + +// try { +// await LoginApi.silentRefresh(); +// await authCheck(); +// } catch (refreshError) { +// console.error("Token refresh failed:", refreshError); +// setUserInfo(null); +// router.push("/auth/login"); +// } +// } +// })(); +// }, []); + +// return <>{children}; +// }; + +// export { AuthWrapper }; diff --git a/apps/dm/src/entities/auth/lib/token-manager.ts b/apps/dm/src/entities/auth/lib/token-manager.ts new file mode 100644 index 000000000..40d32f33e --- /dev/null +++ b/apps/dm/src/entities/auth/lib/token-manager.ts @@ -0,0 +1,59 @@ +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; +import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie"; + +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); + } + + 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, + 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(); diff --git a/apps/dm/src/entities/auth/model/login-interface.ts b/apps/dm/src/entities/auth/model/login-interface.ts new file mode 100644 index 000000000..5aaf53044 --- /dev/null +++ b/apps/dm/src/entities/auth/model/login-interface.ts @@ -0,0 +1,9 @@ +export interface ILogin { + userId: string; + password: string; +} + +export interface LoginService { + login(payload: ILogin): Promise; + silentRefresh(): Promise; +} diff --git a/apps/dm/src/shared/config/api.ts b/apps/dm/src/shared/config/api.ts new file mode 100644 index 000000000..3eba9fa91 --- /dev/null +++ b/apps/dm/src/shared/config/api.ts @@ -0,0 +1,56 @@ +import { PROXY_HOST } from "./env"; + +export const BASE_URL = `${PROXY_HOST}/server`; + +export const ENDPOINT = { + ATTACH_SERVICE: { + TUS_UPLOAD: "/attach-service/api/v1/tus/upload", + EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor", + IMAGES: "/attach-service/api/v1/images", + MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail", + ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", + DOWNLOAD: "/attach-service/api/v1/atflcd/download", + STREAM_VIDEO: "/attach-service/api/v1/stream/video", + }, + USER_SERVICE: { + LOGIN: "/user-service/login", + USERS: "/user-service/api/v1/users", + SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", + AUTHORIZATIONS: "/user-service/api/v1/authorizations", + ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", + ROLES: "/user-service/api/v1/roles", + }, + CMS_SERVICE: { + SITES: "/cms-service/api/v1/sites", + IPS: "/cms-service/api/v1/site-ips", + DOMAINS: "/cms-service/api/v1/site-dmns", + POSTINGS: "/cms-service/api/v1/site-pstgs", + CODE: "/cms-service/api/v1/code/cache", + MENUS: "/cms-service/api/v1/menus", + MENU_ROLES: "/cms-service/api/v1/menu-roles", + MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", + CONTENTS: "/cms-service/api/v1/contents", + CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", + ORGANIZATIONS: "/cms-service/api/v1/ognzs", + USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", + IMAGES: "/cms-service/api/v1/images", + BANNERS: "/cms-service/api/v1/bnrs", + TEMPLATE: "/cms-service/api/v1/tmplt", + TERMS: "/cms-service/api/v1/trms", + LWPO_CODE: "/cms-service/api/v1/lwpo-cds", + UP_CODE: "/cms-service/api/v1/up-codes", + POPUPS: "/cms-service/api/v1/popups", + SCHEDULES: "/cms-service/api/v1/schdls", + SURVEY: "/cms-service/api/v1/srvy", + SURVEY_QUESTION: "/cms-service/api/v1/srvy-qitem", + SURVEY_ARTICLE: "/cms-service/api/v1/srvy-artcl", + }, + BOARD_SERVICE: { + BBS: "/board-service/api/v1/bbs", + BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt", + POSTS: "/board-service/api/v1/posts", + POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", + }, +}; + +export const JWT_EXPIRATION_TIME = 1800000; diff --git a/apps/dm/src/shared/config/env.ts b/apps/dm/src/shared/config/env.ts new file mode 100644 index 000000000..b01588df0 --- /dev/null +++ b/apps/dm/src/shared/config/env.ts @@ -0,0 +1,13 @@ +export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "DM"; +export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; +export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; +export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL ||'http://con.mind-one.co.kr:11035/api/v1'; +export const SERVER_API_FO_URL = process.env.NEXT_PUBLIC_FO_SERVER_API_URL||' http://con.mind-one.co.kr:11082/api/v1'; +export const SERVER_API_WR_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL||' http://con.mind-one.co.kr:11080/api/v1'; +export const SERVER_API_CS_URL = process.env.NEXT_PUBLIC_CS_SERVER_API_URL||'http://dev.onasys.co.kr:8000'; +export const SERVER_API_CS_TUS_URL = process.env.NEXT_PUBLIC_CS_TUS_UPLOAD ||'/attach-service/api/v1/tus/upload' +export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +export const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; +export const 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 SERVER_SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL; \ No newline at end of file diff --git a/apps/dm/src/shared/config/index.ts b/apps/dm/src/shared/config/index.ts new file mode 100644 index 000000000..cd83447d5 --- /dev/null +++ b/apps/dm/src/shared/config/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./env"; diff --git a/apps/dm/src/shared/lib/cookie.ts b/apps/dm/src/shared/lib/cookie.ts new file mode 100644 index 000000000..4bc0fe142 --- /dev/null +++ b/apps/dm/src/shared/lib/cookie.ts @@ -0,0 +1,31 @@ + +import Cookies from "js-cookie"; + + +export function getCookie(name: string): string | undefined { + if (typeof window === "undefined") { + return undefined; + } + return Cookies.get(name); +} + +export function setCookie( + name: string, + value: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.set(name, value, options); +} + +export function removeCookie( + name: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.remove(name, options); +} From 68f468fa2cf44990845cf16acaa47fef2b0b17dc Mon Sep 17 00:00:00 2001 From: yschoi Date: Tue, 22 Jul 2025 15:12:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?output:=20"standalone",=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/dm/next.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dm/next.config.ts b/apps/dm/next.config.ts index 99301dab6..4d171bc44 100644 --- a/apps/dm/next.config.ts +++ b/apps/dm/next.config.ts @@ -2,6 +2,7 @@ import type { NextConfig } from "next"; import webpack from "webpack"; const nextConfig: NextConfig = { + output: "standalone", eslint: { ignoreDuringBuilds: true, },