From 555342d377b189de47c5766954bca7321bf86244 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 21 Dec 2025 20:01:42 -0500 Subject: [PATCH] update frontend --- public/pwaInstall.js | 21 +- .../AuthContext.jsx.swbak.20251219-170456 | 268 ------------- .../Layout/TopBar.jsx.swbak.20251219-170456 | 379 ------------------ 3 files changed, 9 insertions(+), 659 deletions(-) delete mode 100644 src/auth/AuthContext.jsx.swbak.20251219-170456 delete mode 100644 src/components/Layout/TopBar.jsx.swbak.20251219-170456 diff --git a/public/pwaInstall.js b/public/pwaInstall.js index d427d8c..9acba9b 100644 --- a/public/pwaInstall.js +++ b/public/pwaInstall.js @@ -1,28 +1,25 @@ // src/pwaInstall.js let deferredPrompt = null; -// Écoute l'événement natif PWA (se déclenche quand Chrome pense que l'app est installable) +// Stocke l'événement natif quand Chrome le déclenche window.addEventListener('beforeinstallprompt', (e) => { - // Empêche le prompt auto (on veut le contrôler avec le bouton) + // Empêche le prompt auto (on veut le contrôler) e.preventDefault(); deferredPrompt = e; - console.log('PWA install prompt ready - waiting for user click'); + console.log('PWA: Install prompt ready - ready to trigger on button click'); }); -// Fonction appelée quand on clique sur le bouton "Install" +// Fonction appelée par le bouton "Install" export function promptInstall() { if (deferredPrompt) { - deferredPrompt.prompt(); // Affiche le prompt natif "Add to home screen" + console.log('PWA: Triggering native install prompt'); + deferredPrompt.prompt(); deferredPrompt.userChoice.then((choiceResult) => { - if (choiceResult.outcome === 'accepted') { - console.log('User accepted PWA install'); - } else { - console.log('User dismissed PWA install'); - } + console.log('PWA Install outcome:', choiceResult.outcome); deferredPrompt = null; }); } else { - console.log('No install prompt available yet - try again later'); - alert('PWA installation not ready yet. Try again in a few seconds.'); + console.log('PWA: No prompt ready yet - wait for beforeinstallprompt event'); + alert('PWA not ready yet. Try scrolling or interacting more with the site.'); } } \ No newline at end of file diff --git a/src/auth/AuthContext.jsx.swbak.20251219-170456 b/src/auth/AuthContext.jsx.swbak.20251219-170456 deleted file mode 100644 index 179037f..0000000 --- a/src/auth/AuthContext.jsx.swbak.20251219-170456 +++ /dev/null @@ -1,268 +0,0 @@ -import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"; -import { clearAuth, guessUsernameFromToken, jwtExpSeconds, loadAuth, saveAuth } from "./authStorage"; - -const AuthCtx = createContext(null); - -function apiBase() { - return (import.meta?.env?.VITE_API_BASE_URL || "").replace(/\/+$/, ""); -} - -async function apiFetch(path, opts = {}) { - const url = apiBase() + path; - const res = await fetch(url, { - credentials: "include", - ...opts, - headers: { - "Content-Type": "application/json", - ...(opts.headers || {}), - }, - }); - return res; -} - -// tolerant whoami: accepts {authenticated:true} OR {username:""} OR {user:""} OR just ok+token -async function whoami(accessToken) { - const res = await apiFetch("/api/whoami", { - method: "GET", - headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {}, - }); - - const text = await res.text().catch(() => ""); - let json = null; - try { - json = text ? JSON.parse(text) : null; - } catch { - json = null; - } - - const uname = - (json && (json.username || json.user || json.preferred_username)) || - guessUsernameFromToken(accessToken) || - ""; - - const authed = - !!res.ok && - (json?.authenticated === true || json?.ok === true || !!json?.username || !!json?.user || !!uname); - - return { ok: !!res.ok, status: res.status, json, text, authenticated: authed, username: uname }; -} - -async function refreshTokens(refreshToken) { - const res = await apiFetch("/api/refresh", { - method: "POST", - body: JSON.stringify({ refresh_token: refreshToken }), - }); - - const text = await res.text().catch(() => ""); - let json = null; - try { - json = text ? JSON.parse(text) : null; - } catch { - json = null; - } - - return { ok: res.ok, status: res.status, json, text }; -} - -function nowSec() { - return Math.floor(Date.now() / 1000); -} - -export function AuthProvider({ children }) { - const [status, setStatus] = useState("loading"); // loading | anon | auth - const [user, setUser] = useState(null); // { username } - const [tokens, setTokens] = useState(null); // { access_token, refresh_token, ... } - const [error, setError] = useState(""); - const refreshTimer = useRef(null); - - function setAnon(msg = "") { - setStatus("anon"); - setUser(null); - setTokens(null); - setError(msg || ""); - clearAuth(); - } - - function setAuthFromTokens(t, usernameGuess = "") { - setTokens(t); - saveAuth({ ...t, obtained_at: Date.now() }); - const u = usernameGuess || guessUsernameFromToken(t?.access_token) || ""; - setUser(u ? { username: u } : null); - setStatus("auth"); - setError(""); - } - - async function ensureFreshSession(existing) { - if (!existing?.access_token) return { ok: false, reason: "no_access_token" }; - - const exp = jwtExpSeconds(existing.access_token); - const skew = 60; - const isExpiredOrSoon = !exp || exp <= nowSec() + skew; - - if (isExpiredOrSoon) { - if (!existing.refresh_token) return { ok: false, reason: "expired_no_refresh" }; - - const rr = await refreshTokens(existing.refresh_token); - if (!rr.ok || !rr.json?.access_token) { - return { ok: false, reason: `refresh_failed_${rr.status}`, detail: rr.text }; - } - - const merged = { ...existing, ...rr.json }; - if (!merged.refresh_token) merged.refresh_token = existing.refresh_token; - - setAuthFromTokens(merged); - existing = merged; - } else { - setAuthFromTokens(existing); - } - - const w = await whoami(existing.access_token); - if (w.ok && w.authenticated) { - setUser(w.username ? { username: w.username } : null); - setStatus("auth"); - setError(""); - return { ok: true }; - } - - return { ok: false, reason: `whoami_${w.status}`, detail: w.text || "" }; - } - - function scheduleAutoRefresh() { - if (refreshTimer.current) { - clearInterval(refreshTimer.current); - refreshTimer.current = null; - } - - refreshTimer.current = setInterval(async () => { - const cur = loadAuth(); - if (!cur?.access_token) return; - - const exp = jwtExpSeconds(cur.access_token); - if (!exp) return; - - if (exp <= nowSec() + 90) { - if (!cur.refresh_token) { - setAnon("Session expirée (pas de refresh_token)."); - return; - } - const rr = await refreshTokens(cur.refresh_token); - if (!rr.ok || !rr.json?.access_token) { - setAnon("Session expirée. Please login again."); - return; - } - const merged = { ...cur, ...rr.json }; - if (!merged.refresh_token) merged.refresh_token = cur.refresh_token; - setAuthFromTokens(merged); - } - }, 30000); - } - - async function restore() { - setStatus("loading"); - setError(""); - const saved = loadAuth(); - if (!saved?.access_token) { - setAnon(""); - return; - } - - const r = await ensureFreshSession(saved); - if (!r.ok) { - setAnon("Login desynced. Please login again."); - return; - } - - scheduleAutoRefresh(); - } - - async function login(username, password) { - setError(""); - setStatus("loading"); - - const res = await apiFetch("/api/login", { - method: "POST", - body: JSON.stringify({ username, password }), - }); - - const text = await res.text().catch(() => ""); - let j = null; - try { - j = text ? JSON.parse(text) : null; - } catch { - j = null; - } - - if (!res.ok || !j?.access_token) { - setAnon(j?.error_description || j?.error || text || "Login failed"); - return { ok: false, status: res.status, json: j, text }; - } - - const t = { ...j, obtained_at: Date.now() }; - setAuthFromTokens(t); - scheduleAutoRefresh(); - - const w = await whoami(t.access_token); - if (!w.ok || !w.authenticated) { - setAnon("Login desynced. Please login again."); - return { ok: false, status: w.status, json: w.json, text: w.text }; - } - - setUser(w.username ? { username: w.username } : null); - setStatus("auth"); - return { ok: true }; - } - - function logout() { - if (refreshTimer.current) clearInterval(refreshTimer.current); - refreshTimer.current = null; - setAnon(""); - } - - useEffect(() => { - restore(); - const onStorage = (e) => { - if (e.key === "sociowire:auth") restore(); - }; - window.addEventListener("storage", onStorage); - return () => window.removeEventListener("storage", onStorage); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Backward-compatible fields expected by TopBar/MapView - const value = useMemo(() => { - const loading = status === "loading"; - const authenticated = status === "auth"; - const username = user?.username || ""; - const token = tokens?.access_token || ""; - - return { - // internal - status, - user, - tokens, - error, - - // expected by UI - loading, - authenticated, - username, - token, - - lastError: error || "", - lastInfo: "", - needsEmailVerification: false, - resendVerificationEmail: async () => {}, - - login, - logout, - restore, - setError, - }; - }, [status, user, tokens, error]); - - return {children}; -} - -export function useAuth() { - return useContext(AuthCtx); -} diff --git a/src/components/Layout/TopBar.jsx.swbak.20251219-170456 b/src/components/Layout/TopBar.jsx.swbak.20251219-170456 deleted file mode 100644 index 058ea18..0000000 --- a/src/components/Layout/TopBar.jsx.swbak.20251219-170456 +++ /dev/null @@ -1,379 +0,0 @@ -import React, { useState, useEffect, useMemo } from "react"; -import { useAuth } from "../../auth/AuthContext"; -import { registerUser } from "../../api/client"; - -const THEME_LABELS = { dark: "Dark", blue: "Blue", light: "Light" }; -const LAST_SIGNUP_USER_KEY = "sociowire:lastSignupUser"; - -const KC_BASE = - (import.meta?.env?.VITE_KEYCLOAK_BASE_URL) || "https://auth.sociowire.com"; -const KC_REALM = - (import.meta?.env?.VITE_KEYCLOAK_REALM) || "sociowire"; -const KC_CLIENT = - (import.meta?.env?.VITE_KEYCLOAK_CLIENT_ID) || "sociowire"; - -function isEmailVerifyError(raw) { - const msg = String(raw || "").toLowerCase(); - return ( - msg.includes("not fully set up") || - msg.includes("verify") || - msg.includes("verification") || - msg.includes("required action") || - (msg.includes("email") && msg.includes("verified")) - ); -} - -function normalizeAuthMessage(raw) { - const msg = String(raw || "").trim(); - if (!msg) return ""; - - const lower = msg.toLowerCase(); - - if (isEmailVerifyError(lower)) { - return "Ton compte est créé, mais l’email n’est pas confirmé. Clique “Vérifier mon email” (Keycloak), puis utilise le lien reçu par email."; - } - - if ( - lower.includes("invalid user credentials") || - lower.includes("bad credentials") || - lower.includes("invalid_grant") - ) { - return "Mauvais username ou mot de passe."; - } - - if (msg.length > 220) return msg.slice(0, 220) + "…"; - return msg; -} - -function buildKeycloakAuthUrl() { - const redirectUri = `${window.location.origin}/auth/verified`; - const url = - `${KC_BASE}/realms/${encodeURIComponent(KC_REALM)}` + - `/protocol/openid-connect/auth` + - `?client_id=${encodeURIComponent(KC_CLIENT)}` + - `&redirect_uri=${encodeURIComponent(redirectUri)}` + - `&response_type=code&scope=openid`; - return url; -} - -export default function TopBar({ theme = "dark", onChangeTheme }) { - const { - loading, - authenticated, - username, - login, - logout, - resendVerificationEmail, - lastError, - lastInfo, - } = 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 [signupInfo, setSignupInfo] = useState(""); - const [signupLoading, setSignupLoading] = useState(false); - - const prettyError = useMemo(() => normalizeAuthMessage(lastError), [lastError]); - const showVerifyHelp = useMemo(() => isEmailVerifyError(lastError), [lastError]); - - const isVerifiedPage = - typeof window !== "undefined" && window.location?.pathname === "/auth/verified"; - - const openModal = (nextMode) => { - setMode(nextMode); - setShowAuth(true); - setSignupError(""); - setSignupInfo(""); - }; - - const closeModal = () => { - setShowAuth(false); - setLoginPass(""); - setSignupError(""); - setSignupInfo(""); - }; - - // ✅ If error happens, open modal so errors are not in topbar - useEffect(() => { - if (prettyError) { - setMode("login"); - setShowAuth(true); - } - }, [prettyError]); - - // ✅ On /auth/verified: open login + prefill username - useEffect(() => { - if (!isVerifiedPage) return; - setMode("login"); - setShowAuth(true); - - try { - const lastU = localStorage.getItem(LAST_SIGNUP_USER_KEY) || ""; - if (lastU && !loginUser) setLoginUser(lastU); - } catch {} - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVerifiedPage]); - - // ✅ After login on /auth/verified, go home - useEffect(() => { - if (!authenticated) return; - if (typeof window === "undefined") return; - const path = window.location?.pathname || ""; - if (path === "/auth/verified") { - try { window.history.replaceState({}, "", "/"); } catch {} - } - }, [authenticated]); - - const handleLoginSubmit = async (e) => { - e.preventDefault(); - await login(loginUser, loginPass); - }; - - const handleSignupSubmit = async (e) => { - e.preventDefault(); - setSignupError(""); - setSignupInfo(""); - - 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(), - }); - - try { localStorage.setItem(LAST_SIGNUP_USER_KEY, regUser.trim()); } catch {} - - // ✅ Do NOT auto-login (verify email flow) - setSignupInfo("Compte créé ✅ Vérifie ton email (spam inclus), puis connecte-toi."); - setMode("login"); - setLoginUser(regUser.trim()); - setLoginPass(""); - } catch (err) { - console.error(err); - setSignupError(err.message || "Registration error."); - } finally { - setSignupLoading(false); - } - }; - - const NoticeBox = ({ variant, children }) => ( -
- {children} -
- ); - - return ( - <> -
-
-
SW
- -
-
SOCIOWIRE.com
-
Wired to life
-
-
- -
-
- {["dark", "blue", "light"].map((t) => ( - - ))} -
- - {authenticated ? ( -
- {/* ✅ one single user chip */} -
- - - - {username || "user"} - - - Connecté - -
- - -
- ) : ( -
- - -
- )} -
-
- - {showAuth && ( -
-
e.stopPropagation()}> -
- - - -
- - {/* ✅ Verified page success message */} - {isVerifiedPage && !authenticated && ( - - ✅ Email confirmé. Connecte-toi pour continuer. - - )} - - {/* ✅ Any error/info now lives INSIDE modal */} - {prettyError && ( - - - {prettyError} - - )} - - {!prettyError && lastInfo && ( - - - {lastInfo} - - )} - - {/* ✅ Email verify helper buttons */} - {showVerifyHelp && ( - -
-
- - Vérification email requise -
-
- Ton login “modal” ne peut pas compléter l’action Keycloak. Ouvre la page Keycloak, - clique “proceed”, puis clique le lien reçu par email. -
-
- - Vérifier mon email (Keycloak) - - {typeof resendVerificationEmail === "function" && ( - - )} -
-
-
- )} - - {mode === "login" ? ( -
- - - -
- ) : ( -
-
- - -
- - - - - {signupError && {signupError}} - {signupInfo && {signupInfo}} - - -
- )} -
-
- )} - - ); -}