From 9abf38326e981c8ca2786b088857bdcd45a7d4c7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 17 Dec 2025 00:25:50 -0500 Subject: [PATCH] nwnage --- = | 0 fix.sh | 427 --------------- src/auth/AuthContext.jsx | 56 +- src/auth/AuthContext.jsx.bak.1765947497 | 246 +++++++++ src/auth/AuthContext.jsx.bak.1765948491 | 234 +++++++++ src/auth/AuthContext.jsx.bak.1765948522 | 234 +++++++++ src/auth/AuthContext.jsx.bak.1765948573 | 234 +++++++++ src/components/Layout/TopBar.jsx | 2 + .../Layout/TopBar.jsx.bak.1765948462 | 222 ++++++++ .../Layout/TopBar.jsx.bak.1765948508 | 223 ++++++++ utils/fix.sh | 496 +++++++++++++++--- utils/patch_backend_refresh.sh | 119 +++++ utils/patch_frontend_auth_to_backend.sh | 38 ++ utils/patch_show_auth_errors.sh | 13 + 14 files changed, 1997 insertions(+), 547 deletions(-) delete mode 100644 = delete mode 100644 fix.sh create mode 100644 src/auth/AuthContext.jsx.bak.1765947497 create mode 100644 src/auth/AuthContext.jsx.bak.1765948491 create mode 100644 src/auth/AuthContext.jsx.bak.1765948522 create mode 100644 src/auth/AuthContext.jsx.bak.1765948573 create mode 100644 src/components/Layout/TopBar.jsx.bak.1765948462 create mode 100644 src/components/Layout/TopBar.jsx.bak.1765948508 create mode 100755 utils/patch_backend_refresh.sh create mode 100755 utils/patch_frontend_auth_to_backend.sh create mode 100755 utils/patch_show_auth_errors.sh diff --git a/= b/= deleted file mode 100644 index e69de29..0000000 diff --git a/fix.sh b/fix.sh deleted file mode 100644 index 399141a..0000000 --- a/fix.sh +++ /dev/null @@ -1,427 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Run from frontend repo root -# (the folder that contains src/) -test -d "src" || { echo "❌ Run this from the FRONTEND repo root (where ./src exists)"; exit 1; } - -TS="$(date +%Y%m%d-%H%M%S)" -echo "✅ Patching SocioWire frontend (timestamp: $TS)" - -# backup -for f in \ - src/components/Map/useMapCore.js \ - src/components/Map/usePostsEngine.js -do - if [ -f "$f" ]; then - cp -a "$f" "$f.bak.$TS" - echo "📦 Backup: $f -> $f.bak.$TS" - else - echo "⚠️ Missing file: $f (will be created)" - fi -done - -# ----------------------------- -# FILE: src/components/Map/useMapCore.js -# ----------------------------- -mkdir -p src/components/Map -cat > src/components/Map/useMapCore.js <<'EOF' -import { useEffect, useRef, useState } from "react"; -import maplibregl from "maplibre-gl"; -import { LAST_VIEW_KEY } from "./mapConfig"; -import { getViewFromMap } from "./mapGeo"; -import { clearAllMarkers, applyOcclusionForExpanded } from "./markerManager"; - -// MapLibre base style by theme -function getBaseStyle(theme) { - const t = theme || "dark"; - let tiles; - - switch (t) { - case "light": - tiles = ["https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"]; - break; - case "blue": - tiles = ["https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png"]; - break; - case "dark": - default: - tiles = ["https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"]; - break; - } - - return { - version: 8, - sources: { - "carto-base": { - type: "raster", - tiles, - tileSize: 256, - attribution: "© OpenStreetMap contributors © CARTO", - }, - }, - layers: [ - { - id: "carto-base-layer", - type: "raster", - source: "carto-base", - }, - ], - }; -} - -export function useMapCore(theme) { - const containerRef = useRef(null); - const mapRef = useRef(null); - - const markersRef = useRef([]); - const expandedElRef = useRef(null); - - // default large radius - const [viewParams, setViewParams] = useState({ - center: null, - radiusKm: 750, - }); - - const [hasLastView, setHasLastView] = useState(false); - - function closeExpandedIfAny() { - const el = expandedElRef.current; - if (el && el.__renderCompact) el.__renderCompact(); - expandedElRef.current = null; - } - - function safeUpdateViewParams(map) { - try { - const vp = getViewFromMap(map); - if (vp && Array.isArray(vp.center) && vp.center.length === 2) { - setViewParams(vp); - } - } catch { - // ignore - } - } - - useEffect(() => { - if (!containerRef.current) return; - - const map = new maplibregl.Map({ - container: containerRef.current, - style: getBaseStyle(theme || "dark"), - center: [-95, 40], - zoom: 3.5, - pitch: 0, - bearing: 0, - antialias: true, - }); - - // Restore last view if available - let hadLastView = false; - try { - const raw = localStorage.getItem(LAST_VIEW_KEY); - if (raw) { - const v = JSON.parse(raw); - if ( - typeof v.lat === "number" && - typeof v.lon === "number" && - typeof v.zoom === "number" - ) { - map.setCenter([v.lon, v.lat]); - map.setZoom(v.zoom); - hadLastView = true; - } - } - } catch {} - setHasLastView(hadLastView); - - map.addControl(new maplibregl.NavigationControl(), "top-right"); - mapRef.current = map; - - // Re-occlude markers when post expanded - const reOcclude = () => { - const el = expandedElRef.current; - if (el) applyOcclusionForExpanded(markersRef, el); - }; - map.on("move", reOcclude); - map.on("zoom", reOcclude); - - // Close expanded when clicking/dragging map - map.on("click", () => closeExpandedIfAny()); - map.on("dragstart", () => closeExpandedIfAny()); - - // ✅ CRITICAL FIX: - // Set view params ASAP and also on load/idle/moveend. - // On some devices/prod, relying on 'load' only can miss initial fetch triggers. - const update = () => safeUpdateViewParams(map); - - map.on("load", update); - map.on("idle", update); - map.on("moveend", () => { - update(); - try { - const vp = getViewFromMap(map); - const center = vp.center; - localStorage.setItem( - LAST_VIEW_KEY, - JSON.stringify({ - lat: center[1], - lon: center[0], - zoom: map.getZoom(), - }) - ); - } catch {} - }); - - // Also do an early update next frame (helps first load) - requestAnimationFrame(update); - - return () => { - clearAllMarkers(markersRef, expandedElRef); - map.remove(); - mapRef.current = null; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Theme style switch - useEffect(() => { - const map = mapRef.current; - if (!map) return; - - // setStyle triggers style reload; we still want viewParams updated after - map.setStyle(getBaseStyle(theme || "dark")); - - // update view params after style swap (next frame) - requestAnimationFrame(() => { - try { - safeUpdateViewParams(map); - } catch {} - }); - }, [theme]); - - return { - containerRef, - mapRef, - markersRef, - expandedElRef, - viewParams, - hasLastView, - }; -} -EOF - -# ----------------------------- -# FILE: src/components/Map/usePostsEngine.js -# ----------------------------- -cat > src/components/Map/usePostsEngine.js <<'EOF' -import { useCallback, useEffect, useRef, useState } from "react"; -import { fetchPosts } from "../../api/client"; // keep existing endpoint for now -import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter"; -import { haversineKm } from "./mapGeo"; -import { createMarkerForPost, clearAllMarkers } from "./markerManager"; - -/** - * Posts engine: - * - keeps a cache (allPostsRef) - * - applies filters (cat/sub/time) for map + Sociowall - * - fetches by zone - * - * ✅ FIX: first page load must fetch even if viewParams.center is null. - * We fallback to map.getCenter() and default radius 750. - */ -export function usePostsEngine({ - mapRef, - viewParams, - mainFilter, - subFilter, - timeFilter, - markersRef, - expandedElRef, -}) { - const allPostsRef = useRef([]); - - const lastFetchRef = useRef({ - center: null, - radiusKm: null, - filterKey: "", - }); - - const [status, setStatus] = useState("Loading posts..."); - const [visiblePosts, setVisiblePosts] = useState([]); - const [loadingPosts, setLoadingPosts] = useState(false); - const [loadError, setLoadError] = useState(""); - - const rebuildMarkersForFilters = useCallback( - (tf) => { - const map = mapRef.current; - if (!map) return; - - clearAllMarkers(markersRef, expandedElRef); - - const posts = allPostsRef.current || []; - const catCode = categoryCode(mainFilter); - - const visible = posts.filter((p) => { - if (catCode) { - const pc = (p.category || p.Category || "").toString().toUpperCase(); - if (pc !== catCode) return false; - } - if (!matchesSubFilter(p, subFilter)) return false; - if (!matchesTimeFilter(p.created_at || p.CreatedAt, tf)) return false; - return true; - }); - - visible.forEach((post) => { - createMarkerForPost(post, mapRef, markersRef, expandedElRef); - }); - - setVisiblePosts(visible); - setStatus(visible.length ? "" : "No posts found."); - }, - [mapRef, markersRef, expandedElRef, mainFilter, subFilter] - ); - - useEffect(() => { - rebuildMarkersForFilters(timeFilter); - }, [timeFilter, mainFilter, subFilter, rebuildMarkersForFilters]); - - useEffect(() => { - const map = mapRef.current; - if (!map) return; - - let cancelled = false; - - async function load() { - // ✅ fallback center/radius if viewParams not ready yet - const centerArr = - (viewParams && Array.isArray(viewParams.center) && viewParams.center.length === 2) - ? viewParams.center - : (() => { - const c = map.getCenter(); - return [c.lng, c.lat]; - })(); - - const radiusKm = - (viewParams && typeof viewParams.radiusKm === "number" && viewParams.radiusKm > 0) - ? viewParams.radiusKm - : 750; - - const [lng, lat] = centerArr; - - const filterKey = `${mainFilter}|${subFilter}|${timeFilter}`; - const last = lastFetchRef.current; - - if (last.center && last.filterKey === filterKey) { - const [lastLng, lastLat] = last.center; - const distKm = haversineKm(lastLat, lastLng, lat, lng); - - const lastR = last.radiusKm || 0; - const radiusChanged = - !lastR || Math.abs(radiusKm - lastR) / Math.max(lastR, 1) >= 0.25; - - const minMoveKm = Math.max(50, radiusKm * 0.25); - - if (!radiusChanged && distKm < minMoveKm) { - return; - } - } - - try { - setStatus("Loading posts..."); - setLoadingPosts(true); - setLoadError(""); - - const catCode = categoryCode(mainFilter); - const subCatParam = - subFilter && subFilter.toUpperCase() !== "ALL" && subFilter !== "All" - ? subFilter - : ""; - - const newPosts = await fetchPosts({ - category: catCode, - subCategory: subCatParam, - time: "", - lat, - lon: lng, - radiusKm, - }); - - if (cancelled) return; - - const existing = allPostsRef.current || []; - const byId = new Map(); - - for (const p of existing) { - if (p && typeof p.id !== "undefined") byId.set(p.id, p); - } - if (Array.isArray(newPosts)) { - for (const p of newPosts) { - if (p && typeof p.id !== "undefined" && !byId.has(p.id)) { - byId.set(p.id, p); - } - } - } - - allPostsRef.current = Array.from(byId.values()); - - lastFetchRef.current = { - center: [...centerArr], - radiusKm, - filterKey, - }; - - rebuildMarkersForFilters(timeFilter); - setLoadingPosts(false); - } catch (err) { - console.error("Erreur chargement posts:", err); - if (!cancelled) { - setStatus("Error loading posts."); - setLoadError("Error loading posts."); - setLoadingPosts(false); - } - } - } - - load(); - - return () => { - cancelled = true; - }; - }, [viewParams, mainFilter, subFilter, timeFilter, mapRef, rebuildMarkersForFilters, markersRef, expandedElRef]); - - const handleIncomingPost = useCallback( - (p) => { - allPostsRef.current = [...allPostsRef.current, p]; - - const catCode = categoryCode(mainFilter); - if (catCode) { - const pc = (p.category || p.Category || "").toString().toUpperCase(); - if (pc !== catCode) return; - } - if (!matchesSubFilter(p, subFilter)) return; - if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) return; - - createMarkerForPost(p, mapRef, markersRef, expandedElRef); - setVisiblePosts((current) => [...current, p]); - }, - [mainFilter, subFilter, timeFilter, mapRef, markersRef, expandedElRef] - ); - - return { - status, - visiblePosts, - loadingPosts, - loadError, - handleIncomingPost, - }; -} -EOF - -echo "✅ Files written." - -echo "" -echo "Next:" -echo " npm run dev" -echo "or build/deploy as you do normally." -echo "" -echo "Tip: open DevTools Network -> you should see /api/posts on first page load." diff --git a/src/auth/AuthContext.jsx b/src/auth/AuthContext.jsx index 695ed74..6f58d5f 100644 --- a/src/auth/AuthContext.jsx +++ b/src/auth/AuthContext.jsx @@ -2,16 +2,13 @@ import React, { createContext, useContext, useEffect, useRef, useState } from "r const AuthContext = createContext(null); -// Keycloak config -const KC_BASE = "https://web.sociowire.com:8443"; -const KC_REALM = "sociowire"; -const KC_CLIENT_ID = "sociowire-frontend"; +// Backend API (same origin) +const API_BASE = "/api"; const TOKEN_KEY = "sociowire:access_token"; const REFRESH_KEY = "sociowire:refresh_token"; const USERNAME_KEY = "sociowire:username"; -// --- helpers --- function decodeJwtPayload(token) { try { const parts = token.split("."); @@ -30,26 +27,17 @@ function willExpireSoon(token, withinSeconds = 90) { return p.exp - now <= withinSeconds; } -async function refreshWithKeycloak(refreshToken) { - const params = new URLSearchParams(); - params.set("grant_type", "refresh_token"); - params.set("client_id", KC_CLIENT_ID); - params.set("refresh_token", refreshToken); - - const res = await fetch(`${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`, { +async function backendRefresh(refreshToken) { + const res = await fetch(`${API_BASE}/refresh`, { method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: params.toString(), + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ refresh_token: refreshToken }), }); const text = await res.text(); if (!res.ok) { - let msg = text; - try { - const j = JSON.parse(text); - msg = j.error_description || j.error || text; - } catch {} - throw new Error(`${res.status} ${msg}`); + throw new Error(`${res.status} ${text}`); } const data = JSON.parse(text); @@ -111,7 +99,7 @@ export function AuthProvider({ children }) { return true; } - const { accessToken, refreshToken } = await refreshWithKeycloak(storedRefresh); + const { accessToken, refreshToken } = await backendRefresh(storedRefresh); persistAuth({ accessToken, refreshToken, user: storedUser }); return true; } catch (e) { @@ -152,7 +140,7 @@ export function AuthProvider({ children }) { if (!willExpireSoon(t, 90)) return; - const { accessToken, refreshToken } = await refreshWithKeycloak(rt); + const { accessToken, refreshToken } = await backendRefresh(rt); const u = localStorage.getItem(USERNAME_KEY) || usernameRef.current || ""; persistAuth({ accessToken, refreshToken, user: u }); } catch (e) { @@ -172,29 +160,20 @@ export function AuthProvider({ children }) { return; } - const params = new URLSearchParams(); - params.set("grant_type", "password"); - params.set("client_id", KC_CLIENT_ID); - params.set("username", u); - params.set("password", pass); - try { setLoading(true); - const res = await fetch(`${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`, { + const res = await fetch(`${API_BASE}/login`, { method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: params.toString(), + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ username: u, password: pass }), }); const text = await res.text(); if (!res.ok) { - let msg = "Invalid credentials or auth error."; - try { - const j = JSON.parse(text); - msg = j.error_description || j.error || msg; - } catch {} - clearAuth(`${res.status} ${msg}`); + console.error("[AUTH] /api/login failed", res.status, text); + setLastError(`${res.status} login failed`); return; } @@ -203,7 +182,7 @@ export function AuthProvider({ children }) { const refreshToken = data.refresh_token; if (!accessToken) { - clearAuth("No access token in response."); + setLastError("No access token in response."); return; } @@ -211,6 +190,7 @@ export function AuthProvider({ children }) { setLastError(""); } catch (e) { console.error("[AUTH] network error", e); + setLastError("Network error during login."); clearAuth("Network error during login."); } finally { setLoading(false); diff --git a/src/auth/AuthContext.jsx.bak.1765947497 b/src/auth/AuthContext.jsx.bak.1765947497 new file mode 100644 index 0000000..695ed74 --- /dev/null +++ b/src/auth/AuthContext.jsx.bak.1765947497 @@ -0,0 +1,246 @@ +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; + +const AuthContext = createContext(null); + +// Keycloak config +const KC_BASE = "https://web.sociowire.com:8443"; +const KC_REALM = "sociowire"; +const KC_CLIENT_ID = "sociowire-frontend"; + +const TOKEN_KEY = "sociowire:access_token"; +const REFRESH_KEY = "sociowire:refresh_token"; +const USERNAME_KEY = "sociowire:username"; + +// --- helpers --- +function decodeJwtPayload(token) { + try { + const parts = token.split("."); + if (parts.length < 2) return null; + const json = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")); + return JSON.parse(json); + } catch { + return null; + } +} + +function willExpireSoon(token, withinSeconds = 90) { + const p = decodeJwtPayload(token); + if (!p || !p.exp) return true; + const now = Math.floor(Date.now() / 1000); + return p.exp - now <= withinSeconds; +} + +async function refreshWithKeycloak(refreshToken) { + const params = new URLSearchParams(); + params.set("grant_type", "refresh_token"); + params.set("client_id", KC_CLIENT_ID); + params.set("refresh_token", refreshToken); + + const res = await fetch(`${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: params.toString(), + }); + + const text = await res.text(); + if (!res.ok) { + let msg = text; + try { + const j = JSON.parse(text); + msg = j.error_description || j.error || text; + } catch {} + throw new Error(`${res.status} ${msg}`); + } + + const data = JSON.parse(text); + if (!data.access_token) throw new Error("No access_token in refresh response"); + return { + accessToken: data.access_token, + refreshToken: data.refresh_token || refreshToken, + }; +} + +export function AuthProvider({ children }) { + const [booting, setBooting] = useState(true); + const [loading, setLoading] = useState(false); + const [authenticated, setAuthenticated] = useState(false); + const [username, setUsername] = useState(""); + const [token, setToken] = useState(null); + const [lastError, setLastError] = useState(""); + + const usernameRef = useRef(""); + + function persistAuth({ accessToken, refreshToken, user }) { + setToken(accessToken); + setAuthenticated(true); + setUsername(user); + usernameRef.current = user; + try { + localStorage.setItem(TOKEN_KEY, accessToken); + if (refreshToken) localStorage.setItem(REFRESH_KEY, refreshToken); + localStorage.setItem(USERNAME_KEY, user); + } catch {} + } + + function clearAuth(msg = "") { + setAuthenticated(false); + setToken(null); + setUsername(""); + usernameRef.current = ""; + setLastError(msg); + try { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_KEY); + localStorage.removeItem(USERNAME_KEY); + } catch {} + } + + async function trySilentRefreshIfNeeded() { + try { + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedRefresh = localStorage.getItem(REFRESH_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + + if (!storedToken || !storedRefresh) return false; + + if (!willExpireSoon(storedToken, 30)) { + setToken(storedToken); + setUsername(storedUser); + usernameRef.current = storedUser; + setAuthenticated(true); + return true; + } + + const { accessToken, refreshToken } = await refreshWithKeycloak(storedRefresh); + persistAuth({ accessToken, refreshToken, user: storedUser }); + return true; + } catch (e) { + console.warn("[AUTH] silent refresh failed:", e); + clearAuth("Session expired. Please login again."); + return false; + } + } + + useEffect(() => { + (async () => { + try { + const ok = await trySilentRefreshIfNeeded(); + if (!ok) { + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + if (storedToken) { + setToken(storedToken); + setUsername(storedUser); + usernameRef.current = storedUser; + setAuthenticated(true); + } + } + } finally { + setBooting(false); + } + })(); + }, []); + + useEffect(() => { + if (!authenticated) return; + + const interval = setInterval(async () => { + try { + const t = localStorage.getItem(TOKEN_KEY); + const rt = localStorage.getItem(REFRESH_KEY); + if (!t || !rt) return; + + if (!willExpireSoon(t, 90)) return; + + const { accessToken, refreshToken } = await refreshWithKeycloak(rt); + const u = localStorage.getItem(USERNAME_KEY) || usernameRef.current || ""; + persistAuth({ accessToken, refreshToken, user: u }); + } catch (e) { + console.warn("[AUTH] auto refresh failed:", e); + clearAuth("Session expired. Please login again."); + } + }, 60_000); + + return () => clearInterval(interval); + }, [authenticated]); + + async function login(user, pass) { + setLastError(""); + const u = (user || "").trim(); + if (!u || !pass) { + setLastError("Username and password required."); + return; + } + + const params = new URLSearchParams(); + params.set("grant_type", "password"); + params.set("client_id", KC_CLIENT_ID); + params.set("username", u); + params.set("password", pass); + + try { + setLoading(true); + + const res = await fetch(`${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: params.toString(), + }); + + const text = await res.text(); + if (!res.ok) { + let msg = "Invalid credentials or auth error."; + try { + const j = JSON.parse(text); + msg = j.error_description || j.error || msg; + } catch {} + clearAuth(`${res.status} ${msg}`); + return; + } + + const data = JSON.parse(text); + const accessToken = data.access_token; + const refreshToken = data.refresh_token; + + if (!accessToken) { + clearAuth("No access token in response."); + return; + } + + persistAuth({ accessToken, refreshToken, user: u }); + setLastError(""); + } catch (e) { + console.error("[AUTH] network error", e); + clearAuth("Network error during login."); + } finally { + setLoading(false); + } + } + + function logout() { + clearAuth(""); + } + + return ( + + {children} + + ); +} + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used inside "); + return ctx; +} diff --git a/src/auth/AuthContext.jsx.bak.1765948491 b/src/auth/AuthContext.jsx.bak.1765948491 new file mode 100644 index 0000000..037648e --- /dev/null +++ b/src/auth/AuthContext.jsx.bak.1765948491 @@ -0,0 +1,234 @@ +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; + +const AuthContext = createContext(null); + +// Keycloak config + +const TOKEN_KEY = "sociowire:access_token"; +const REFRESH_KEY = "sociowire:refresh_token"; +const USERNAME_KEY = "sociowire:username"; + +// --- helpers --- +function decodeJwtPayload(token) { + try { + const parts = token.split("."); + if (parts.length < 2) return null; + const json = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")); + return JSON.parse(json); + } catch { + return null; + } +} + +function willExpireSoon(token, withinSeconds = 90) { + const p = decodeJwtPayload(token); + if (!p || !p.exp) return true; + const now = Math.floor(Date.now() / 1000); + return p.exp - now <= withinSeconds; +} + +async function refreshViaBackend(refreshToken) { + const res = await fetch("/api/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ refresh_token: refreshToken }), + }); + + const text = await res.text(); + if (!res.ok) { + let msg = text; + try { + const j = JSON.parse(text); + msg = j.error_description || j.error || text; + } catch {} + throw new Error(` `); + } + + const data = JSON.parse(text); + if (!data.access_token) throw new Error("No access_token in refresh response"); + return { + accessToken: data.access_token, + refreshToken: data.refresh_token || refreshToken, + }; +} + +export function AuthProvider({ children }) { + const [booting, setBooting] = useState(true); + const [loading, setLoading] = useState(false); + const [authenticated, setAuthenticated] = useState(false); + const [username, setUsername] = useState(""); + const [token, setToken] = useState(null); + const [lastError, setLastError] = useState(""); + + const usernameRef = useRef(""); + + function persistAuth({ accessToken, refreshToken, user }) { + setToken(accessToken); + setAuthenticated(true); + setUsername(user); + usernameRef.current = user; + try { + localStorage.setItem(TOKEN_KEY, accessToken); + if (refreshToken) localStorage.setItem(REFRESH_KEY, refreshToken); + localStorage.setItem(USERNAME_KEY, user); + } catch {} + } + + function clearAuth(msg = "") { + setAuthenticated(false); + setToken(null); + setUsername(""); + usernameRef.current = ""; + setLastError(msg); + try { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_KEY); + localStorage.removeItem(USERNAME_KEY); + } catch {} + } + + async function trySilentRefreshIfNeeded() { + try { + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedRefresh = localStorage.getItem(REFRESH_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + + if (!storedToken || !storedRefresh) return false; + + if (!willExpireSoon(storedToken, 30)) { + setToken(storedToken); + setUsername(storedUser); + usernameRef.current = storedUser; + setAuthenticated(true); + return true; + } + + const { accessToken, refreshToken } = await refreshViaBackend(storedRefresh); + persistAuth({ accessToken, refreshToken, user: storedUser }); + return true; + } catch (e) { + console.warn("[AUTH] silent refresh failed:", e); + clearAuth("Session expired. Please login again."); + return false; + } + } + + useEffect(() => { + (async () => { + try { + const ok = await trySilentRefreshIfNeeded(); + if (!ok) { + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + if (storedToken) { + setToken(storedToken); + setUsername(storedUser); + usernameRef.current = storedUser; + setAuthenticated(true); + } + } + } finally { + setBooting(false); + } + })(); + }, []); + + useEffect(() => { + if (!authenticated) return; + + const interval = setInterval(async () => { + try { + const t = localStorage.getItem(TOKEN_KEY); + const rt = localStorage.getItem(REFRESH_KEY); + if (!t || !rt) return; + + if (!willExpireSoon(t, 90)) return; + + const { accessToken, refreshToken } = await refreshViaBackend(rt); + const u = localStorage.getItem(USERNAME_KEY) || usernameRef.current || ""; + persistAuth({ accessToken, refreshToken, user: u }); + } catch (e) { + console.warn("[AUTH] auto refresh failed:", e); + clearAuth("Session expired. Please login again."); + } + }, 60_000); + + return () => clearInterval(interval); + }, [authenticated]); + + async function login(user, pass) { + setLastError(""); + const u = (user || "").trim(); + if (!u || !pass) { + setLastError("Username and password required."); + return; + } + + try { + setLoading(true); + + const res = await fetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ username: u, password: pass }), + }); + + const text = await res.text(); + if (!res.ok) { + let msg = "Invalid credentials or auth error."; + try { + const j = JSON.parse(text); + msg = j.error_description || j.error || msg; + } catch {} + clearAuth(` `); + return; + } + + const data = JSON.parse(text); + const accessToken = data.access_token; + const refreshToken = data.refresh_token; + + if (!accessToken) { + clearAuth("No access token in response."); + return; + } + + persistAuth({ accessToken, refreshToken, user: u }); + setLastError(""); + } catch (e) { + console.error("[AUTH] network error", e); + clearAuth("Network error during login."); + } finally { + setLoading(false); + } + } + + function logout() { + clearAuth(""); + } + + return ( + + {children} + + ); +} + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used inside "); + return ctx; +} diff --git a/src/auth/AuthContext.jsx.bak.1765948522 b/src/auth/AuthContext.jsx.bak.1765948522 new file mode 100644 index 0000000..037648e --- /dev/null +++ b/src/auth/AuthContext.jsx.bak.1765948522 @@ -0,0 +1,234 @@ +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; + +const AuthContext = createContext(null); + +// Keycloak config + +const TOKEN_KEY = "sociowire:access_token"; +const REFRESH_KEY = "sociowire:refresh_token"; +const USERNAME_KEY = "sociowire:username"; + +// --- helpers --- +function decodeJwtPayload(token) { + try { + const parts = token.split("."); + if (parts.length < 2) return null; + const json = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")); + return JSON.parse(json); + } catch { + return null; + } +} + +function willExpireSoon(token, withinSeconds = 90) { + const p = decodeJwtPayload(token); + if (!p || !p.exp) return true; + const now = Math.floor(Date.now() / 1000); + return p.exp - now <= withinSeconds; +} + +async function refreshViaBackend(refreshToken) { + const res = await fetch("/api/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ refresh_token: refreshToken }), + }); + + const text = await res.text(); + if (!res.ok) { + let msg = text; + try { + const j = JSON.parse(text); + msg = j.error_description || j.error || text; + } catch {} + throw new Error(` `); + } + + const data = JSON.parse(text); + if (!data.access_token) throw new Error("No access_token in refresh response"); + return { + accessToken: data.access_token, + refreshToken: data.refresh_token || refreshToken, + }; +} + +export function AuthProvider({ children }) { + const [booting, setBooting] = useState(true); + const [loading, setLoading] = useState(false); + const [authenticated, setAuthenticated] = useState(false); + const [username, setUsername] = useState(""); + const [token, setToken] = useState(null); + const [lastError, setLastError] = useState(""); + + const usernameRef = useRef(""); + + function persistAuth({ accessToken, refreshToken, user }) { + setToken(accessToken); + setAuthenticated(true); + setUsername(user); + usernameRef.current = user; + try { + localStorage.setItem(TOKEN_KEY, accessToken); + if (refreshToken) localStorage.setItem(REFRESH_KEY, refreshToken); + localStorage.setItem(USERNAME_KEY, user); + } catch {} + } + + function clearAuth(msg = "") { + setAuthenticated(false); + setToken(null); + setUsername(""); + usernameRef.current = ""; + setLastError(msg); + try { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_KEY); + localStorage.removeItem(USERNAME_KEY); + } catch {} + } + + async function trySilentRefreshIfNeeded() { + try { + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedRefresh = localStorage.getItem(REFRESH_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + + if (!storedToken || !storedRefresh) return false; + + if (!willExpireSoon(storedToken, 30)) { + setToken(storedToken); + setUsername(storedUser); + usernameRef.current = storedUser; + setAuthenticated(true); + return true; + } + + const { accessToken, refreshToken } = await refreshViaBackend(storedRefresh); + persistAuth({ accessToken, refreshToken, user: storedUser }); + return true; + } catch (e) { + console.warn("[AUTH] silent refresh failed:", e); + clearAuth("Session expired. Please login again."); + return false; + } + } + + useEffect(() => { + (async () => { + try { + const ok = await trySilentRefreshIfNeeded(); + if (!ok) { + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + if (storedToken) { + setToken(storedToken); + setUsername(storedUser); + usernameRef.current = storedUser; + setAuthenticated(true); + } + } + } finally { + setBooting(false); + } + })(); + }, []); + + useEffect(() => { + if (!authenticated) return; + + const interval = setInterval(async () => { + try { + const t = localStorage.getItem(TOKEN_KEY); + const rt = localStorage.getItem(REFRESH_KEY); + if (!t || !rt) return; + + if (!willExpireSoon(t, 90)) return; + + const { accessToken, refreshToken } = await refreshViaBackend(rt); + const u = localStorage.getItem(USERNAME_KEY) || usernameRef.current || ""; + persistAuth({ accessToken, refreshToken, user: u }); + } catch (e) { + console.warn("[AUTH] auto refresh failed:", e); + clearAuth("Session expired. Please login again."); + } + }, 60_000); + + return () => clearInterval(interval); + }, [authenticated]); + + async function login(user, pass) { + setLastError(""); + const u = (user || "").trim(); + if (!u || !pass) { + setLastError("Username and password required."); + return; + } + + try { + setLoading(true); + + const res = await fetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ username: u, password: pass }), + }); + + const text = await res.text(); + if (!res.ok) { + let msg = "Invalid credentials or auth error."; + try { + const j = JSON.parse(text); + msg = j.error_description || j.error || msg; + } catch {} + clearAuth(` `); + return; + } + + const data = JSON.parse(text); + const accessToken = data.access_token; + const refreshToken = data.refresh_token; + + if (!accessToken) { + clearAuth("No access token in response."); + return; + } + + persistAuth({ accessToken, refreshToken, user: u }); + setLastError(""); + } catch (e) { + console.error("[AUTH] network error", e); + clearAuth("Network error during login."); + } finally { + setLoading(false); + } + } + + function logout() { + clearAuth(""); + } + + return ( + + {children} + + ); +} + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used inside "); + return ctx; +} diff --git a/src/auth/AuthContext.jsx.bak.1765948573 b/src/auth/AuthContext.jsx.bak.1765948573 new file mode 100644 index 0000000..037648e --- /dev/null +++ b/src/auth/AuthContext.jsx.bak.1765948573 @@ -0,0 +1,234 @@ +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; + +const AuthContext = createContext(null); + +// Keycloak config + +const TOKEN_KEY = "sociowire:access_token"; +const REFRESH_KEY = "sociowire:refresh_token"; +const USERNAME_KEY = "sociowire:username"; + +// --- helpers --- +function decodeJwtPayload(token) { + try { + const parts = token.split("."); + if (parts.length < 2) return null; + const json = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")); + return JSON.parse(json); + } catch { + return null; + } +} + +function willExpireSoon(token, withinSeconds = 90) { + const p = decodeJwtPayload(token); + if (!p || !p.exp) return true; + const now = Math.floor(Date.now() / 1000); + return p.exp - now <= withinSeconds; +} + +async function refreshViaBackend(refreshToken) { + const res = await fetch("/api/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ refresh_token: refreshToken }), + }); + + const text = await res.text(); + if (!res.ok) { + let msg = text; + try { + const j = JSON.parse(text); + msg = j.error_description || j.error || text; + } catch {} + throw new Error(` `); + } + + const data = JSON.parse(text); + if (!data.access_token) throw new Error("No access_token in refresh response"); + return { + accessToken: data.access_token, + refreshToken: data.refresh_token || refreshToken, + }; +} + +export function AuthProvider({ children }) { + const [booting, setBooting] = useState(true); + const [loading, setLoading] = useState(false); + const [authenticated, setAuthenticated] = useState(false); + const [username, setUsername] = useState(""); + const [token, setToken] = useState(null); + const [lastError, setLastError] = useState(""); + + const usernameRef = useRef(""); + + function persistAuth({ accessToken, refreshToken, user }) { + setToken(accessToken); + setAuthenticated(true); + setUsername(user); + usernameRef.current = user; + try { + localStorage.setItem(TOKEN_KEY, accessToken); + if (refreshToken) localStorage.setItem(REFRESH_KEY, refreshToken); + localStorage.setItem(USERNAME_KEY, user); + } catch {} + } + + function clearAuth(msg = "") { + setAuthenticated(false); + setToken(null); + setUsername(""); + usernameRef.current = ""; + setLastError(msg); + try { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_KEY); + localStorage.removeItem(USERNAME_KEY); + } catch {} + } + + async function trySilentRefreshIfNeeded() { + try { + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedRefresh = localStorage.getItem(REFRESH_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + + if (!storedToken || !storedRefresh) return false; + + if (!willExpireSoon(storedToken, 30)) { + setToken(storedToken); + setUsername(storedUser); + usernameRef.current = storedUser; + setAuthenticated(true); + return true; + } + + const { accessToken, refreshToken } = await refreshViaBackend(storedRefresh); + persistAuth({ accessToken, refreshToken, user: storedUser }); + return true; + } catch (e) { + console.warn("[AUTH] silent refresh failed:", e); + clearAuth("Session expired. Please login again."); + return false; + } + } + + useEffect(() => { + (async () => { + try { + const ok = await trySilentRefreshIfNeeded(); + if (!ok) { + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + if (storedToken) { + setToken(storedToken); + setUsername(storedUser); + usernameRef.current = storedUser; + setAuthenticated(true); + } + } + } finally { + setBooting(false); + } + })(); + }, []); + + useEffect(() => { + if (!authenticated) return; + + const interval = setInterval(async () => { + try { + const t = localStorage.getItem(TOKEN_KEY); + const rt = localStorage.getItem(REFRESH_KEY); + if (!t || !rt) return; + + if (!willExpireSoon(t, 90)) return; + + const { accessToken, refreshToken } = await refreshViaBackend(rt); + const u = localStorage.getItem(USERNAME_KEY) || usernameRef.current || ""; + persistAuth({ accessToken, refreshToken, user: u }); + } catch (e) { + console.warn("[AUTH] auto refresh failed:", e); + clearAuth("Session expired. Please login again."); + } + }, 60_000); + + return () => clearInterval(interval); + }, [authenticated]); + + async function login(user, pass) { + setLastError(""); + const u = (user || "").trim(); + if (!u || !pass) { + setLastError("Username and password required."); + return; + } + + try { + setLoading(true); + + const res = await fetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ username: u, password: pass }), + }); + + const text = await res.text(); + if (!res.ok) { + let msg = "Invalid credentials or auth error."; + try { + const j = JSON.parse(text); + msg = j.error_description || j.error || msg; + } catch {} + clearAuth(` `); + return; + } + + const data = JSON.parse(text); + const accessToken = data.access_token; + const refreshToken = data.refresh_token; + + if (!accessToken) { + clearAuth("No access token in response."); + return; + } + + persistAuth({ accessToken, refreshToken, user: u }); + setLastError(""); + } catch (e) { + console.error("[AUTH] network error", e); + clearAuth("Network error during login."); + } finally { + setLoading(false); + } + } + + function logout() { + clearAuth(""); + } + + return ( + + {children} + + ); +} + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used inside "); + return ctx; +} diff --git a/src/components/Layout/TopBar.jsx b/src/components/Layout/TopBar.jsx index 6be5434..7d1cf2f 100644 --- a/src/components/Layout/TopBar.jsx +++ b/src/components/Layout/TopBar.jsx @@ -181,6 +181,7 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { + {lastError &&
{lastError}
} ) : (
@@ -212,6 +213,7 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { + {lastError &&
{lastError}
}
)} diff --git a/src/components/Layout/TopBar.jsx.bak.1765948462 b/src/components/Layout/TopBar.jsx.bak.1765948462 new file mode 100644 index 0000000..6be5434 --- /dev/null +++ b/src/components/Layout/TopBar.jsx.bak.1765948462 @@ -0,0 +1,222 @@ +import React, { useState, useEffect } from "react"; +import { useAuth } from "../../auth/AuthContext"; +import { registerUser } from "../../api/client"; + +const THEME_LABELS = { + dark: "Dark", + blue: "Blue", + light: "Light", +}; + +export default function TopBar({ theme = "dark", onChangeTheme }) { + const { loading, authenticated, username, login, logout, lastError } = useAuth(); + + const [showAuth, setShowAuth] = useState(false); + const [mode, setMode] = useState("login"); + + const [loginUser, setLoginUser] = useState(""); + const [loginPass, setLoginPass] = useState(""); + + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [regUser, setRegUser] = useState(""); + const [regEmail, setRegEmail] = useState(""); + const [regPass, setRegPass] = useState(""); + const [signupError, setSignupError] = useState(""); + const [signupLoading, setSignupLoading] = useState(false); + + let authLabel = "AUTH: anon"; + if (loading) authLabel = "AUTH: loading…"; + else if (authenticated) authLabel = `AUTH: user=${username || "?"}`; + + const openModal = (nextMode) => { + setMode(nextMode); + setShowAuth(true); + setSignupError(""); + }; + + const closeModal = () => { + setShowAuth(false); + setLoginPass(""); + setSignupError(""); + }; + + useEffect(() => { + if (authenticated) setShowAuth(false); + }, [authenticated]); + + const handleLoginSubmit = async (e) => { + e.preventDefault(); + await login(loginUser, loginPass); + }; + + const handleSignupSubmit = async (e) => { + e.preventDefault(); + setSignupError(""); + if ( + !firstName.trim() || + !lastName.trim() || + !regUser.trim() || + !regEmail.trim() || + !regPass + ) { + setSignupError("First name, last name, username, email and password are required."); + return; + } + try { + setSignupLoading(true); + await registerUser({ + username: regUser.trim(), + email: regEmail.trim(), + password: regPass, + firstName: firstName.trim(), + lastName: lastName.trim(), + }); + await login(regUser.trim(), regPass); + } catch (err) { + console.error(err); + setSignupError(err.message || "Registration error."); + } finally { + setSignupLoading(false); + } + }; + + return ( + <> +
+
+
SW
+
+
SOCIOWIRE.com
+
Wired to life
+
+ {authLabel} + {lastError && ( + + ({lastError}) + + )} +
+
+
+ +
+
+ {["dark", "blue", "light"].map((t) => ( + + ))} +
+ + {authenticated ? ( +
+ {username} + +
+ ) : ( +
+ + +
+ )} +
+
+ + {showAuth && ( +
+
e.stopPropagation()}> +
+ + + +
+ + {mode === "login" ? ( +
+ + + +
+ ) : ( +
+
+ + +
+ + + + + {signupError &&
{signupError}
} + + +
+ )} +
+
+ )} + + ); +} diff --git a/src/components/Layout/TopBar.jsx.bak.1765948508 b/src/components/Layout/TopBar.jsx.bak.1765948508 new file mode 100644 index 0000000..228fe93 --- /dev/null +++ b/src/components/Layout/TopBar.jsx.bak.1765948508 @@ -0,0 +1,223 @@ +import React, { useState, useEffect } from "react"; +import { useAuth } from "../../auth/AuthContext"; +import { registerUser } from "../../api/client"; + +const THEME_LABELS = { + dark: "Dark", + blue: "Blue", + light: "Light", +}; + +export default function TopBar({ theme = "dark", onChangeTheme }) { + const { loading, authenticated, username, login, logout, lastError } = useAuth(); + + const [showAuth, setShowAuth] = useState(false); + const [mode, setMode] = useState("login"); + + const [loginUser, setLoginUser] = useState(""); + const [loginPass, setLoginPass] = useState(""); + + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [regUser, setRegUser] = useState(""); + const [regEmail, setRegEmail] = useState(""); + const [regPass, setRegPass] = useState(""); + const [signupError, setSignupError] = useState(""); + const [signupLoading, setSignupLoading] = useState(false); + + let authLabel = "AUTH: anon"; + if (loading) authLabel = "AUTH: loading…"; + else if (authenticated) authLabel = `AUTH: user=${username || "?"}`; + + const openModal = (nextMode) => { + setMode(nextMode); + setShowAuth(true); + setSignupError(""); + }; + + const closeModal = () => { + setShowAuth(false); + setLoginPass(""); + setSignupError(""); + }; + + useEffect(() => { + if (authenticated) setShowAuth(false); + }, [authenticated]); + + const handleLoginSubmit = async (e) => { + e.preventDefault(); + await login(loginUser, loginPass); + }; + + const handleSignupSubmit = async (e) => { + e.preventDefault(); + setSignupError(""); + if ( + !firstName.trim() || + !lastName.trim() || + !regUser.trim() || + !regEmail.trim() || + !regPass + ) { + setSignupError("First name, last name, username, email and password are required."); + return; + } + try { + setSignupLoading(true); + await registerUser({ + username: regUser.trim(), + email: regEmail.trim(), + password: regPass, + firstName: firstName.trim(), + lastName: lastName.trim(), + }); + await login(regUser.trim(), regPass); + } catch (err) { + console.error(err); + setSignupError(err.message || "Registration error."); + } finally { + setSignupLoading(false); + } + }; + + return ( + <> +
+
+
SW
+
+
SOCIOWIRE.com
+
Wired to life
+
+ {authLabel} + {lastError && ( + + ({lastError}) + + )} +
+
+
+ +
+
+ {["dark", "blue", "light"].map((t) => ( + + ))} +
+ + {authenticated ? ( +
+ {username} + +
+ ) : ( +
+ + +
+ )} +
+
+ + {showAuth && ( +
+
e.stopPropagation()}> +
+ + + +
+ + {mode === "login" ? ( +
+ + + + {lastError &&
{lastError}
} +
+ ) : ( +
+
+ + +
+ + + + + {signupError &&
{signupError}
} + + +
+ )} +
+
+ )} + + ); +} diff --git a/utils/fix.sh b/utils/fix.sh index 6dc69dd..399141a 100644 --- a/utils/fix.sh +++ b/utils/fix.sh @@ -1,95 +1,427 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -echo "=== SOCIOWIRE LOGOS + METADATA INSTALLER ===" +# Run from frontend repo root +# (the folder that contains src/) +test -d "src" || { echo "❌ Run this from the FRONTEND repo root (where ./src exists)"; exit 1; } -# --- REQUIRE Imagemagick --- -if ! command -v convert &> /dev/null -then - echo "Imagemagick non installé — installation..." - sudo apt install -y imagemagick -fi +TS="$(date +%Y%m%d-%H%M%S)" +echo "✅ Patching SocioWire frontend (timestamp: $TS)" -# --- CREATE FOLDERS --- -mkdir -p public/icons +# backup +for f in \ + src/components/Map/useMapCore.js \ + src/components/Map/usePostsEngine.js +do + if [ -f "$f" ]; then + cp -a "$f" "$f.bak.$TS" + echo "📦 Backup: $f -> $f.bak.$TS" + else + echo "⚠️ Missing file: $f (will be created)" + fi +done -# --- COPY MASTER LOGO --- +# ----------------------------- +# FILE: src/components/Map/useMapCore.js +# ----------------------------- +mkdir -p src/components/Map +cat > src/components/Map/useMapCore.js <<'EOF' +import { useEffect, useRef, useState } from "react"; +import maplibregl from "maplibre-gl"; +import { LAST_VIEW_KEY } from "./mapConfig"; +import { getViewFromMap } from "./mapGeo"; +import { clearAllMarkers, applyOcclusionForExpanded } from "./markerManager"; -# --- GENERATE ICONS --- -echo "Génération des icônes…" +// MapLibre base style by theme +function getBaseStyle(theme) { + const t = theme || "dark"; + let tiles; -convert public/icons/logo-master.png -resize 16x16 public/icons/favicon-16.png -convert public/icons/logo-master.png -resize 32x32 public/icons/favicon-32.png -convert public/icons/logo-master.png -resize 48x48 public/icons/favicon-48.png -convert public/icons/logo-master.png -resize 180x180 public/icons/apple-touch-icon.png -convert public/icons/logo-master.png -resize 192x192 public/icons/icon-192.png -convert public/icons/logo-master.png -resize 512x512 public/icons/icon-512.png -convert public/icons/logo-master.png -resize 1200x630 public/icons/og-image.png + switch (t) { + case "light": + tiles = ["https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"]; + break; + case "blue": + tiles = ["https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png"]; + break; + case "dark": + default: + tiles = ["https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"]; + break; + } -echo "Icônes générées." - -# --- ADD METADATA INTO index.html --- -echo "Injection des balises SEO et OpenGraph…" - -sed -i '//a \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ -' index.html - -echo "Metadonnées ajoutées." - -# --- INSTALL MANIFEST.JSON FOR PWA + GOOGLE + ANDROID --- -echo "Création du manifest.json…" - -cat << 'EOF' > public/manifest.json -{ - "name": "SocioWire", - "short_name": "SocioWire", - "description": "Real-time posts on a global map — wired to life.", - "start_url": "/", - "display": "standalone", - "background_color": "#020617", - "theme_color": "#0ea5e9", - "icons": [ - { - "src": "/icons/icon-192.png", - "sizes": "192x192", - "type": "image/png" + return { + version: 8, + sources: { + "carto-base": { + type: "raster", + tiles, + tileSize: 256, + attribution: "© OpenStreetMap contributors © CARTO", + }, }, - { - "src": "/icons/icon-512.png", - "sizes": "512x512", - "type": "image/png" + layers: [ + { + id: "carto-base-layer", + type: "raster", + source: "carto-base", + }, + ], + }; +} + +export function useMapCore(theme) { + const containerRef = useRef(null); + const mapRef = useRef(null); + + const markersRef = useRef([]); + const expandedElRef = useRef(null); + + // default large radius + const [viewParams, setViewParams] = useState({ + center: null, + radiusKm: 750, + }); + + const [hasLastView, setHasLastView] = useState(false); + + function closeExpandedIfAny() { + const el = expandedElRef.current; + if (el && el.__renderCompact) el.__renderCompact(); + expandedElRef.current = null; + } + + function safeUpdateViewParams(map) { + try { + const vp = getViewFromMap(map); + if (vp && Array.isArray(vp.center) && vp.center.length === 2) { + setViewParams(vp); + } + } catch { + // ignore } - ] + } + + useEffect(() => { + if (!containerRef.current) return; + + const map = new maplibregl.Map({ + container: containerRef.current, + style: getBaseStyle(theme || "dark"), + center: [-95, 40], + zoom: 3.5, + pitch: 0, + bearing: 0, + antialias: true, + }); + + // Restore last view if available + let hadLastView = false; + try { + const raw = localStorage.getItem(LAST_VIEW_KEY); + if (raw) { + const v = JSON.parse(raw); + if ( + typeof v.lat === "number" && + typeof v.lon === "number" && + typeof v.zoom === "number" + ) { + map.setCenter([v.lon, v.lat]); + map.setZoom(v.zoom); + hadLastView = true; + } + } + } catch {} + setHasLastView(hadLastView); + + map.addControl(new maplibregl.NavigationControl(), "top-right"); + mapRef.current = map; + + // Re-occlude markers when post expanded + const reOcclude = () => { + const el = expandedElRef.current; + if (el) applyOcclusionForExpanded(markersRef, el); + }; + map.on("move", reOcclude); + map.on("zoom", reOcclude); + + // Close expanded when clicking/dragging map + map.on("click", () => closeExpandedIfAny()); + map.on("dragstart", () => closeExpandedIfAny()); + + // ✅ CRITICAL FIX: + // Set view params ASAP and also on load/idle/moveend. + // On some devices/prod, relying on 'load' only can miss initial fetch triggers. + const update = () => safeUpdateViewParams(map); + + map.on("load", update); + map.on("idle", update); + map.on("moveend", () => { + update(); + try { + const vp = getViewFromMap(map); + const center = vp.center; + localStorage.setItem( + LAST_VIEW_KEY, + JSON.stringify({ + lat: center[1], + lon: center[0], + zoom: map.getZoom(), + }) + ); + } catch {} + }); + + // Also do an early update next frame (helps first load) + requestAnimationFrame(update); + + return () => { + clearAllMarkers(markersRef, expandedElRef); + map.remove(); + mapRef.current = null; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Theme style switch + useEffect(() => { + const map = mapRef.current; + if (!map) return; + + // setStyle triggers style reload; we still want viewParams updated after + map.setStyle(getBaseStyle(theme || "dark")); + + // update view params after style swap (next frame) + requestAnimationFrame(() => { + try { + safeUpdateViewParams(map); + } catch {} + }); + }, [theme]); + + return { + containerRef, + mapRef, + markersRef, + expandedElRef, + viewParams, + hasLastView, + }; } EOF -echo "manifest.json généré." +# ----------------------------- +# FILE: src/components/Map/usePostsEngine.js +# ----------------------------- +cat > src/components/Map/usePostsEngine.js <<'EOF' +import { useCallback, useEffect, useRef, useState } from "react"; +import { fetchPosts } from "../../api/client"; // keep existing endpoint for now +import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter"; +import { haversineKm } from "./mapGeo"; +import { createMarkerForPost, clearAllMarkers } from "./markerManager"; -echo "=== INSTALLATION TERMINÉE ===" -echo "Ton site a maintenant :" -echo "✔ Favicon" -echo "✔ Apple Touch Icon" -echo "✔ Android App Icon" -echo "✔ OpenGraph Facebook" -echo "✔ Twitter Card" -echo "✔ SEO Google complet" -echo "✔ PWA Manifest" +/** + * Posts engine: + * - keeps a cache (allPostsRef) + * - applies filters (cat/sub/time) for map + Sociowall + * - fetches by zone + * + * ✅ FIX: first page load must fetch even if viewParams.center is null. + * We fallback to map.getCenter() and default radius 750. + */ +export function usePostsEngine({ + mapRef, + viewParams, + mainFilter, + subFilter, + timeFilter, + markersRef, + expandedElRef, +}) { + const allPostsRef = useRef([]); + + const lastFetchRef = useRef({ + center: null, + radiusKm: null, + filterKey: "", + }); + + const [status, setStatus] = useState("Loading posts..."); + const [visiblePosts, setVisiblePosts] = useState([]); + const [loadingPosts, setLoadingPosts] = useState(false); + const [loadError, setLoadError] = useState(""); + + const rebuildMarkersForFilters = useCallback( + (tf) => { + const map = mapRef.current; + if (!map) return; + + clearAllMarkers(markersRef, expandedElRef); + + const posts = allPostsRef.current || []; + const catCode = categoryCode(mainFilter); + + const visible = posts.filter((p) => { + if (catCode) { + const pc = (p.category || p.Category || "").toString().toUpperCase(); + if (pc !== catCode) return false; + } + if (!matchesSubFilter(p, subFilter)) return false; + if (!matchesTimeFilter(p.created_at || p.CreatedAt, tf)) return false; + return true; + }); + + visible.forEach((post) => { + createMarkerForPost(post, mapRef, markersRef, expandedElRef); + }); + + setVisiblePosts(visible); + setStatus(visible.length ? "" : "No posts found."); + }, + [mapRef, markersRef, expandedElRef, mainFilter, subFilter] + ); + + useEffect(() => { + rebuildMarkersForFilters(timeFilter); + }, [timeFilter, mainFilter, subFilter, rebuildMarkersForFilters]); + + useEffect(() => { + const map = mapRef.current; + if (!map) return; + + let cancelled = false; + + async function load() { + // ✅ fallback center/radius if viewParams not ready yet + const centerArr = + (viewParams && Array.isArray(viewParams.center) && viewParams.center.length === 2) + ? viewParams.center + : (() => { + const c = map.getCenter(); + return [c.lng, c.lat]; + })(); + + const radiusKm = + (viewParams && typeof viewParams.radiusKm === "number" && viewParams.radiusKm > 0) + ? viewParams.radiusKm + : 750; + + const [lng, lat] = centerArr; + + const filterKey = `${mainFilter}|${subFilter}|${timeFilter}`; + const last = lastFetchRef.current; + + if (last.center && last.filterKey === filterKey) { + const [lastLng, lastLat] = last.center; + const distKm = haversineKm(lastLat, lastLng, lat, lng); + + const lastR = last.radiusKm || 0; + const radiusChanged = + !lastR || Math.abs(radiusKm - lastR) / Math.max(lastR, 1) >= 0.25; + + const minMoveKm = Math.max(50, radiusKm * 0.25); + + if (!radiusChanged && distKm < minMoveKm) { + return; + } + } + + try { + setStatus("Loading posts..."); + setLoadingPosts(true); + setLoadError(""); + + const catCode = categoryCode(mainFilter); + const subCatParam = + subFilter && subFilter.toUpperCase() !== "ALL" && subFilter !== "All" + ? subFilter + : ""; + + const newPosts = await fetchPosts({ + category: catCode, + subCategory: subCatParam, + time: "", + lat, + lon: lng, + radiusKm, + }); + + if (cancelled) return; + + const existing = allPostsRef.current || []; + const byId = new Map(); + + for (const p of existing) { + if (p && typeof p.id !== "undefined") byId.set(p.id, p); + } + if (Array.isArray(newPosts)) { + for (const p of newPosts) { + if (p && typeof p.id !== "undefined" && !byId.has(p.id)) { + byId.set(p.id, p); + } + } + } + + allPostsRef.current = Array.from(byId.values()); + + lastFetchRef.current = { + center: [...centerArr], + radiusKm, + filterKey, + }; + + rebuildMarkersForFilters(timeFilter); + setLoadingPosts(false); + } catch (err) { + console.error("Erreur chargement posts:", err); + if (!cancelled) { + setStatus("Error loading posts."); + setLoadError("Error loading posts."); + setLoadingPosts(false); + } + } + } + + load(); + + return () => { + cancelled = true; + }; + }, [viewParams, mainFilter, subFilter, timeFilter, mapRef, rebuildMarkersForFilters, markersRef, expandedElRef]); + + const handleIncomingPost = useCallback( + (p) => { + allPostsRef.current = [...allPostsRef.current, p]; + + const catCode = categoryCode(mainFilter); + if (catCode) { + const pc = (p.category || p.Category || "").toString().toUpperCase(); + if (pc !== catCode) return; + } + if (!matchesSubFilter(p, subFilter)) return; + if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) return; + + createMarkerForPost(p, mapRef, markersRef, expandedElRef); + setVisiblePosts((current) => [...current, p]); + }, + [mainFilter, subFilter, timeFilter, mapRef, markersRef, expandedElRef] + ); + + return { + status, + visiblePosts, + loadingPosts, + loadError, + handleIncomingPost, + }; +} +EOF + +echo "✅ Files written." + +echo "" +echo "Next:" +echo " npm run dev" +echo "or build/deploy as you do normally." +echo "" +echo "Tip: open DevTools Network -> you should see /api/posts on first page load." diff --git a/utils/patch_backend_refresh.sh b/utils/patch_backend_refresh.sh new file mode 100755 index 0000000..c398cfe --- /dev/null +++ b/utils/patch_backend_refresh.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ===== CONFIG ===== +BACKEND_DIR="${BACKEND_DIR:-sociowire-backend}" + +cd "$BACKEND_DIR" + +echo "[1/2] Write auth_refresh.go" +cat > auth_refresh.go <<'GO' +package main + +import ( + "encoding/json" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +func refreshHandler(w http.ResponseWriter, r *http.Request) { + setCommonHeaders(w) + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + type req struct { + RefreshToken string `json:"refresh_token"` + } + var body req + _ = json.NewDecoder(r.Body).Decode(&body) + + rt := strings.TrimSpace(body.RefreshToken) + if rt == "" { + http.Error(w, "refresh_token required", http.StatusBadRequest) + return + } + + kcURL := strings.TrimRight(strings.TrimSpace(os.Getenv("KEYCLOAK_URL")), "/") + realm := strings.TrimSpace(os.Getenv("KEYCLOAK_REALM")) + clientID := strings.TrimSpace(os.Getenv("KC_PUBLIC_CLIENT_ID")) + clientSecret := strings.TrimSpace(os.Getenv("KC_PUBLIC_CLIENT_SECRET")) + + if kcURL == "" || realm == "" || clientID == "" { + http.Error(w, "keycloak env missing", http.StatusBadGateway) + return + } + + tokenURL := kcURL + "/realms/" + realm + "/protocol/openid-connect/token" + + form := url.Values{} + form.Set("grant_type", "refresh_token") + form.Set("client_id", clientID) + form.Set("refresh_token", rt) + if clientSecret != "" { + form.Set("client_secret", clientSecret) + } + + req2, _ := http.NewRequest("POST", tokenURL, strings.NewReader(form.Encode())) + req2.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{Timeout: 8 * time.Second} + resp, err := client.Do(req2) + if err != nil { + http.Error(w, "keycloak refresh error", http.StatusBadGateway) + return + } + defer resp.Body.Close() + + raw, _ := io.ReadAll(resp.Body) + if resp.StatusCode != 200 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write(raw) + return + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(raw) +} +GO + +echo "[2/2] Patch main.go to add /api/refresh route" +if ! grep -q '"/api/refresh"' main.go; then + # Insert after /api/login line if present, else near auth routes block + # This is a simple awk insert. + awk ' + {print} + /mux\.HandleFunc\("\/api\/login", loginHandler\)/ { + print "\tmux.HandleFunc(\"/api/refresh\", refreshHandler)" + } + ' main.go > main.go.tmp + + # If insertion didn’t happen (no match), append under auth routes area: + if ! grep -q '"/api/refresh"' main.go.tmp; then + awk ' + {print} + /mux\.HandleFunc\("\/api\/login", loginHandler\)/==0 && /\/\/ Auth\/debug/ { + # no-op marker + } + ' main.go.tmp > /dev/null 2>&1 || true + # fallback: append near other routes (safe) + sed -i 's|mux.HandleFunc("/api/login", loginHandler)|mux.HandleFunc("/api/login", loginHandler)\n\tmux.HandleFunc("/api/refresh", refreshHandler)|' main.go.tmp || true + fi + + mv main.go.tmp main.go +else + echo "main.go already has /api/refresh" +fi + +echo "DONE backend patch." diff --git a/utils/patch_frontend_auth_to_backend.sh b/utils/patch_frontend_auth_to_backend.sh new file mode 100755 index 0000000..5c03b14 --- /dev/null +++ b/utils/patch_frontend_auth_to_backend.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +FRONTEND_DIR="${FRONTEND_DIR:-sociowire-frontend}" +FILE="$FRONTEND_DIR/src/auth/AuthContext.jsx" + +if [ ! -f "$FILE" ]; then + echo "ERROR: not found: $FILE" + exit 1 +fi + +echo "[1/3] Backup AuthContext.jsx" +cp -a "$FILE" "$FILE.bak.$(date +%s)" + +echo "[2/3] Remove direct Keycloak constants (KC_BASE/KC_REALM/KC_CLIENT_ID) if present" +# delete block lines that define KC_BASE/KC_REALM/KC_CLIENT_ID +sed -i \ + -e '/^const KC_BASE = /d' \ + -e '/^const KC_REALM = /d' \ + -e '/^const KC_CLIENT_ID = /d' \ + "$FILE" + +echo "[3/3] Replace refreshWithKeycloak + login() to use backend /api/login + /api/refresh" + +# Replace function refreshWithKeycloak(...) { ... } with refreshViaBackend(...) +perl -0777 -i -pe ' +s/async function refreshWithKeycloak\([^\)]*\)\s*\{.*?\n\}\n/async function refreshViaBackend(refreshToken) {\n const res = await fetch(\"\/api\/refresh\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application\/json\" },\n credentials: \"include\",\n body: JSON.stringify({ refresh_token: refreshToken }),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let msg = text;\n try {\n const j = JSON.parse(text);\n msg = j.error_description || j.error || text;\n } catch {}\n throw new Error(`${res.status} ${msg}`);\n }\n\n const data = JSON.parse(text);\n if (!data.access_token) throw new Error(\"No access_token in refresh response\");\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token || refreshToken,\n };\n}\n/sms +' "$FILE" + +# Replace any calls to refreshWithKeycloak( with refreshViaBackend( +sed -i 's/refreshWithKeycloak(/refreshViaBackend(/g' "$FILE" + +# Replace login() body to call /api/login +perl -0777 -i -pe ' +s/async function login\([^\)]*\)\s*\{.*?\n\s*\}\n\n\s*function logout\(/async function login(user, pass) {\n setLastError(\"\");\n const u = (user || \"\").trim();\n if (!u || !pass) {\n setLastError(\"Username and password required.\");\n return;\n }\n\n try {\n setLoading(true);\n\n const res = await fetch(\"\/api\/login\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application\/json\" },\n credentials: \"include\",\n body: JSON.stringify({ username: u, password: pass }),\n });\n\n const text = await res.text();\n if (!res.ok) {\n let msg = \"Invalid credentials or auth error.\";\n try {\n const j = JSON.parse(text);\n msg = j.error_description || j.error || msg;\n } catch {}\n clearAuth(`${res.status} ${msg}`);\n return;\n }\n\n const data = JSON.parse(text);\n const accessToken = data.access_token;\n const refreshToken = data.refresh_token;\n\n if (!accessToken) {\n clearAuth(\"No access token in response.\");\n return;\n }\n\n persistAuth({ accessToken, refreshToken, user: u });\n setLastError(\"\");\n } catch (e) {\n console.error(\"[AUTH] network error\", e);\n clearAuth(\"Network error during login.\");\n } finally {\n setLoading(false);\n }\n }\n\n function logout(/sms +' "$FILE" + +echo "DONE frontend patch." diff --git a/utils/patch_show_auth_errors.sh b/utils/patch_show_auth_errors.sh new file mode 100755 index 0000000..270a0f2 --- /dev/null +++ b/utils/patch_show_auth_errors.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail +APP_DIR="${APP_DIR:-sociowire-frontend}" +FILE="$APP_DIR/src/components/Layout/TopBar.jsx" + +cp -a "$FILE" "$FILE.bak.$(date +%s)" + +# Ajoute l'affichage lastError dans le form login (sous le bouton) +perl -0777 -i -pe ' +s|(\s*)|\n {lastError &&
{lastError}
}\n |ms +' "$FILE" + +echo "OK patched: $FILE"