Browse Source

feat: implement cookie handling for HABIB_TOKEN and HABIB_COINS in AuthBridge and update request headers

master
mortezaei 2 months ago
parent
commit
76da723845
  1. 27
      src/app/api/proxy/route.ts
  2. 70
      src/app/layout.tsx
  3. 77
      src/lib/auth-bridge.ts
  4. 65
      src/lib/cookies.ts
  5. 14
      src/lib/http.ts

27
src/app/api/proxy/route.ts

@ -9,6 +9,7 @@ const REQUEST_HEADERS_TO_FORWARD = [
"accept", "accept",
"accept-language", "accept-language",
"authorization", "authorization",
"cookie",
"token", "token",
"content-type", "content-type",
"x-csrf-token", "x-csrf-token",
@ -95,9 +96,18 @@ function getTargetUrl(request: NextRequest) {
return targetUrl; 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) { function getRequestHeaders(request: NextRequest, targetUrl: URL) {
const headers = new Headers(); const headers = new Headers();
const authKey = process.env.NEXT_PUBLIC_AUTH_KEY; const authKey = process.env.NEXT_PUBLIC_AUTH_KEY;
const cookieHeader = request.headers.get("cookie") ?? "";
for (const header of REQUEST_HEADERS_TO_FORWARD) { for (const header of REQUEST_HEADERS_TO_FORWARD) {
const value = request.headers.get(header); 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")) { if (authKey && !headers.has("authorization")) {
headers.set("authorization", `token ${authKey}`);
headers.set("authorization", `Token ${authKey}`);
} }
headers.set("accept-encoding", "identity"); headers.set("accept-encoding", "identity");

70
src/app/layout.tsx

@ -50,26 +50,60 @@ export default function RootLayout({
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.onFlutterResponse = window.onFlutterResponse || function() {}; 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');
}
});
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;
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 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 // Performance monitoring
window.addEventListener('load', function() { window.addEventListener('load', function() {

77
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 { class AuthBridge {
private token: string | null = null; private token: string | null = null;
private coins: number = 0;
private coins = 0;
private isReady = false; private isReady = false;
private readyCallbacks: Array<() => void> = []; private readyCallbacks: Array<() => void> = [];
@ -8,37 +13,61 @@ class AuthBridge {
this.init(); 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() { private init() {
if (typeof window !== 'undefined') {
const win = window as any;
if (typeof window === "undefined") {
return;
}
if (win.HABIB_TOKEN) {
this.token = win.HABIB_TOKEN;
this.coins = win.HABIB_COINS || 0;
this.isReady = true;
this.notifyReady();
} else {
this.waitForInjection();
}
if (this.hydrateFromWindow()) {
this.isReady = true;
this.notifyReady();
return;
} }
this.waitForInjection();
} }
private waitForInjection() { private waitForInjection() {
let attempts = 0; let attempts = 0;
const maxAttempts = 50; 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; this.isReady = true;
clearInterval(checkInterval);
window.clearInterval(checkInterval);
this.notifyReady(); 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.isReady = true;
this.notifyReady(); this.notifyReady();
} }
@ -46,7 +75,7 @@ class AuthBridge {
} }
private notifyReady() { private notifyReady() {
this.readyCallbacks.forEach(cb => cb());
this.readyCallbacks.forEach((cb) => cb());
this.readyCallbacks = []; this.readyCallbacks = [];
} }
@ -59,7 +88,7 @@ class AuthBridge {
} }
public getToken(): string | null { public getToken(): string | null {
return this.token;
return this.token ?? getClientCookie(TOKEN_COOKIE_NAME) ?? getClientCookie("habib_token");
} }
public getCoins(): number { public getCoins(): number {
@ -67,7 +96,7 @@ class AuthBridge {
} }
public isAuthenticated(): boolean { public isAuthenticated(): boolean {
return !!this.token;
return !!this.getToken();
} }
} }

65
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;
}

14
src/lib/http.ts

@ -1,5 +1,4 @@
import axios, { type InternalAxiosRequestConfig } from "axios"; import axios, { type InternalAxiosRequestConfig } from "axios";
import { authBridge } from "./auth-bridge";
const PROXY_PATH_PARAM = "__proxyPath"; const PROXY_PATH_PARAM = "__proxyPath";
const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]); 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); return /^[a-z][a-z\d+\-.]*:\/\//i.test(url);
} }
function isLocalhost() {
function shouldUseProxy() {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return false; return false;
} }
return LOCALHOST_HOSTNAMES.has(window.location.hostname);
return LOCALHOST_HOSTNAMES.has(window.location.hostname) || true;
} }
function withProxyPathParam( function withProxyPathParam(
@ -39,7 +38,7 @@ function withProxyPathParam(
} }
export function getApiRequestUrl(path: string) { export function getApiRequestUrl(path: string) {
if (!isAbsoluteUrl(path) && isLocalhost()) {
if (!isAbsoluteUrl(path) && shouldUseProxy()) {
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
[PROXY_PATH_PARAM]: path, [PROXY_PATH_PARAM]: path,
}); });
@ -51,7 +50,7 @@ export function getApiRequestUrl(path: string) {
} }
export const http = axios.create({ 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: { headers: {
Accept: "application/json", Accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
@ -60,12 +59,13 @@ export const http = axios.create({
}); });
http.interceptors.request.use((config) => { http.interceptors.request.use((config) => {
const token = authBridge.getToken();
const token = authBridge.getToken() ?? getClientCookie("HABIB_TOKEN");
if (token) { if (token) {
config.headers.Authorization = `Token ${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.params = withProxyPathParam(config.params, config.url);
config.url = ""; config.url = "";
} }

Loading…
Cancel
Save