From 76da723845f47901b8e791479d926a3852c23e10 Mon Sep 17 00:00:00 2001 From: mortezaei Date: Fri, 15 May 2026 18:35:06 +0330 Subject: [PATCH] feat: implement cookie handling for HABIB_TOKEN and HABIB_COINS in AuthBridge and update request headers --- src/app/api/proxy/route.ts | 27 ++++++++++++- src/app/layout.tsx | 76 +++++++++++++++++++++++++---------- src/lib/auth-bridge.ts | 81 ++++++++++++++++++++++++++------------ src/lib/cookies.ts | 65 ++++++++++++++++++++++++++++++ src/lib/http.ts | 14 +++---- 5 files changed, 207 insertions(+), 56 deletions(-) create mode 100644 src/lib/cookies.ts diff --git a/src/app/api/proxy/route.ts b/src/app/api/proxy/route.ts index 1e12588..9fa7e54 100644 --- a/src/app/api/proxy/route.ts +++ b/src/app/api/proxy/route.ts @@ -9,6 +9,7 @@ const REQUEST_HEADERS_TO_FORWARD = [ "accept", "accept-language", "authorization", + "cookie", "token", "content-type", "x-csrf-token", @@ -95,9 +96,18 @@ function getTargetUrl(request: NextRequest) { return targetUrl; } +function getCookieValue(cookieHeader: string, name: string) { + const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`(?:^|;\\s*)${escapedName}=([^;]*)`); + const match = cookieHeader.match(pattern); + + return match ? decodeURIComponent(match[1]) : null; +} + function getRequestHeaders(request: NextRequest, targetUrl: URL) { const headers = new Headers(); const authKey = process.env.NEXT_PUBLIC_AUTH_KEY; + const cookieHeader = request.headers.get("cookie") ?? ""; for (const header of REQUEST_HEADERS_TO_FORWARD) { const value = request.headers.get(header); @@ -112,9 +122,22 @@ function getRequestHeaders(request: NextRequest, targetUrl: URL) { } } - // Override with authKey if set and no authorization header exists + // Prefer a cookie-based token when the browser session is anonymous. + if (!headers.has("authorization")) { + const cookieToken = + getCookieValue(cookieHeader, "HABIB_TOKEN") ?? + getCookieValue(cookieHeader, "habib_token") ?? + getCookieValue(cookieHeader, "token") ?? + getCookieValue(cookieHeader, "auth_token"); + + if (cookieToken) { + headers.set("authorization", `Token ${cookieToken}`); + } + } + + // Override with authKey if set and no authorization header exists. if (authKey && !headers.has("authorization")) { - headers.set("authorization", `token ${authKey}`); + headers.set("authorization", `Token ${authKey}`); } headers.set("accept-encoding", "identity"); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2e89d86..5725c1f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -49,28 +49,62 @@ export default function RootLayout({ __html: ` if (typeof window !== 'undefined') { window.onFlutterResponse = window.onFlutterResponse || function() {}; - - // Save to sessionStorage when injected - Object.defineProperty(window, 'HABIB_TOKEN', { - set: function(value) { - this._habib_token = value; - if (value) sessionStorage.setItem('habib_token', value); - }, - get: function() { - return this._habib_token || sessionStorage.getItem('habib_token'); - } - }); - - Object.defineProperty(window, 'HABIB_COINS', { - set: function(value) { - this._habib_coins = value; - if (value) sessionStorage.setItem('habib_coins', String(value)); - }, - get: function() { - return this._habib_coins || parseInt(sessionStorage.getItem('habib_coins') || '0'); + + var HABIB_TOKEN_COOKIE = 'HABIB_TOKEN'; + var HABIB_COINS_COOKIE = 'HABIB_COINS'; + var HABIB_COOKIE_MAX_AGE = 60 * 60 * 24 * 30; + + function writeCookie(name, value) { + if (value === undefined || value === null || value === '') return; + + var secure = window.location.protocol === 'https:'; + var cookie = name + '=' + encodeURIComponent(String(value)) + '; Path=/; Max-Age=' + HABIB_COOKIE_MAX_AGE + '; SameSite=Lax'; + + if (secure) { + cookie += '; Secure'; } - }); - + + document.cookie = cookie; + } + + function readCookie(name) { + var match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\\]\\]/g, '\\\\$&') + '=([^;]*)')); + return match ? decodeURIComponent(match[1]) : ''; + } + + if (!Object.getOwnPropertyDescriptor(window, 'HABIB_TOKEN')) { + Object.defineProperty(window, 'HABIB_TOKEN', { + configurable: true, + set: function(value) { + this._habib_token = value; + if (value) { + writeCookie(HABIB_TOKEN_COOKIE, value); + sessionStorage.setItem(HABIB_TOKEN_COOKIE, value); + } + }, + get: function() { + return this._habib_token || readCookie(HABIB_TOKEN_COOKIE) || sessionStorage.getItem(HABIB_TOKEN_COOKIE); + } + }); + } + + if (!Object.getOwnPropertyDescriptor(window, 'HABIB_COINS')) { + Object.defineProperty(window, 'HABIB_COINS', { + configurable: true, + set: function(value) { + this._habib_coins = value; + if (value !== undefined && value !== null && value !== '') { + writeCookie(HABIB_COINS_COOKIE, value); + sessionStorage.setItem(HABIB_COINS_COOKIE, String(value)); + } + }, + get: function() { + var cookieValue = readCookie(HABIB_COINS_COOKIE); + return this._habib_coins || parseInt(cookieValue || sessionStorage.getItem(HABIB_COINS_COOKIE) || '0'); + } + }); + } + // Performance monitoring window.addEventListener('load', function() { var perfData = performance.getEntriesByType('navigation')[0]; diff --git a/src/lib/auth-bridge.ts b/src/lib/auth-bridge.ts index ed52eae..724ea21 100644 --- a/src/lib/auth-bridge.ts +++ b/src/lib/auth-bridge.ts @@ -1,6 +1,11 @@ +import { getClientCookie } from "./cookies"; + +const TOKEN_COOKIE_NAME = "HABIB_TOKEN"; +const COINS_COOKIE_NAME = "HABIB_COINS"; + class AuthBridge { private token: string | null = null; - private coins: number = 0; + private coins = 0; private isReady = false; private readyCallbacks: Array<() => void> = []; @@ -8,37 +13,61 @@ class AuthBridge { this.init(); } + private hydrateFromWindow() { + if (typeof window === "undefined") { + return false; + } + + const win = window as Window & { + HABIB_TOKEN?: string; + HABIB_COINS?: number; + }; + + const token = + win.HABIB_TOKEN ?? + getClientCookie(TOKEN_COOKIE_NAME) ?? + getClientCookie("habib_token"); + const coinsValue = win.HABIB_COINS; + const coinsCookie = getClientCookie(COINS_COOKIE_NAME) ?? getClientCookie("habib_coins"); + + if (token) { + this.token = token; + this.coins = coinsValue ?? Number(coinsCookie ?? 0); + return true; + } + + return false; + } + private init() { - if (typeof window !== 'undefined') { - const win = window as any; - - if (win.HABIB_TOKEN) { - this.token = win.HABIB_TOKEN; - this.coins = win.HABIB_COINS || 0; - this.isReady = true; - this.notifyReady(); - } else { - this.waitForInjection(); - } + if (typeof window === "undefined") { + return; + } + + if (this.hydrateFromWindow()) { + this.isReady = true; + this.notifyReady(); + return; } + + this.waitForInjection(); } private waitForInjection() { let attempts = 0; const maxAttempts = 50; - - const checkInterval = setInterval(() => { - const win = window as any; - - if (win.HABIB_TOKEN) { - this.token = win.HABIB_TOKEN; - this.coins = win.HABIB_COINS || 0; + + const checkInterval = window.setInterval(() => { + if (this.hydrateFromWindow()) { this.isReady = true; - clearInterval(checkInterval); + window.clearInterval(checkInterval); this.notifyReady(); - } else if (++attempts >= maxAttempts) { - clearInterval(checkInterval); - console.warn('⚠️ Token not received from Flutter'); + return; + } + + if (++attempts >= maxAttempts) { + window.clearInterval(checkInterval); + console.warn("⚠️ Token not received from Flutter"); this.isReady = true; this.notifyReady(); } @@ -46,7 +75,7 @@ class AuthBridge { } private notifyReady() { - this.readyCallbacks.forEach(cb => cb()); + this.readyCallbacks.forEach((cb) => cb()); this.readyCallbacks = []; } @@ -59,7 +88,7 @@ class AuthBridge { } public getToken(): string | null { - return this.token; + return this.token ?? getClientCookie(TOKEN_COOKIE_NAME) ?? getClientCookie("habib_token"); } public getCoins(): number { @@ -67,7 +96,7 @@ class AuthBridge { } public isAuthenticated(): boolean { - return !!this.token; + return !!this.getToken(); } } diff --git a/src/lib/cookies.ts b/src/lib/cookies.ts new file mode 100644 index 0000000..2bd0a92 --- /dev/null +++ b/src/lib/cookies.ts @@ -0,0 +1,65 @@ +const DEFAULT_COOKIE_PATH = "/"; +const DEFAULT_COOKIE_MAX_AGE = 60 * 60 * 24 * 30; + +function isClient() { + return typeof document !== "undefined"; +} + +function escapeCookieName(name: string) { + return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +export function getClientCookie(name: string) { + if (!isClient()) { + return null; + } + + const match = document.cookie.match( + new RegExp(`(?:^|; )${escapeCookieName(name)}=([^;]*)`), + ); + + return match ? decodeURIComponent(match[1]) : null; +} + +export function setClientCookie( + name: string, + value: string, + options?: { + maxAge?: number; + path?: string; + sameSite?: "Lax" | "Strict" | "None"; + secure?: boolean; + }, +) { + if (!isClient()) { + return; + } + + const maxAge = options?.maxAge ?? DEFAULT_COOKIE_MAX_AGE; + const path = options?.path ?? DEFAULT_COOKIE_PATH; + const sameSite = options?.sameSite ?? "Lax"; + const secure = options?.secure ?? window.location.protocol === "https:"; + + let cookie = `${name}=${encodeURIComponent(value)}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`; + + if (secure) { + cookie += "; Secure"; + } + + document.cookie = cookie; +} + +export function deleteClientCookie(name: string, path = DEFAULT_COOKIE_PATH) { + if (!isClient()) { + return; + } + + const secure = window.location.protocol === "https:"; + let cookie = `${name}=; Path=${path}; Max-Age=0; SameSite=Lax`; + + if (secure) { + cookie += "; Secure"; + } + + document.cookie = cookie; +} diff --git a/src/lib/http.ts b/src/lib/http.ts index 5ee745e..7fbe41e 100644 --- a/src/lib/http.ts +++ b/src/lib/http.ts @@ -1,5 +1,4 @@ import axios, { type InternalAxiosRequestConfig } from "axios"; -import { authBridge } from "./auth-bridge"; const PROXY_PATH_PARAM = "__proxyPath"; const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]); @@ -8,12 +7,12 @@ function isAbsoluteUrl(url: string) { return /^[a-z][a-z\d+\-.]*:\/\//i.test(url); } -function isLocalhost() { +function shouldUseProxy() { if (typeof window === "undefined") { return false; } - return LOCALHOST_HOSTNAMES.has(window.location.hostname); + return LOCALHOST_HOSTNAMES.has(window.location.hostname) || true; } function withProxyPathParam( @@ -39,7 +38,7 @@ function withProxyPathParam( } export function getApiRequestUrl(path: string) { - if (!isAbsoluteUrl(path) && isLocalhost()) { + if (!isAbsoluteUrl(path) && shouldUseProxy()) { const searchParams = new URLSearchParams({ [PROXY_PATH_PARAM]: path, }); @@ -51,7 +50,7 @@ export function getApiRequestUrl(path: string) { } export const http = axios.create({ - baseURL: isLocalhost() ? "/api/proxy" : process.env.NEXT_PUBLIC_API_BASE_URL, + baseURL: shouldUseProxy() ? "/api/proxy" : process.env.NEXT_PUBLIC_API_BASE_URL, headers: { Accept: "application/json", "Content-Type": "application/json", @@ -60,12 +59,13 @@ export const http = axios.create({ }); http.interceptors.request.use((config) => { - const token = authBridge.getToken(); + const token = authBridge.getToken() ?? getClientCookie("HABIB_TOKEN"); + if (token) { config.headers.Authorization = `Token ${token}`; } - if (isLocalhost() && config.url && !isAbsoluteUrl(config.url)) { + if (shouldUseProxy() && config.url && !isAbsoluteUrl(config.url)) { config.params = withProxyPathParam(config.params, config.url); config.url = ""; }