From af03d992cb2246b033a9d9dda08ab20a56e378a4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 11 Dec 2025 00:58:23 +0000 Subject: [PATCH] work with kc --- package-lock.json | 10 + src/api/client.js | 10 +- src/auth/AuthContext.jsx | 165 ++++++----- src/components/Layout/TopBar.jsx | 384 +++++++++++++------------- src/components/Map/useUserPosition.js | 29 +- src/main.jsx | 3 +- src/styles/auth-modal.css | 89 ++++++ src/styles/topbar.css | 31 ++- 8 files changed, 431 insertions(+), 290 deletions(-) create mode 100644 src/styles/auth-modal.css diff --git a/package-lock.json b/package-lock.json index 92bd153..5c72180 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1074,6 +1075,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1124,6 +1126,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1246,6 +1249,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1547,6 +1551,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2767,6 +2772,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2852,6 +2858,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2861,6 +2868,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3163,6 +3171,7 @@ "integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/runtime": "0.97.0", "fdir": "^6.5.0", @@ -3285,6 +3294,7 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/api/client.js b/src/api/client.js index bc56594..7ee2c61 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -4,6 +4,9 @@ const API_BASE = "/api"; +/** + * Récupère la liste des posts, avec filtres optionnels. + */ export async function fetchPosts(filters = {}) { const params = new URLSearchParams(); @@ -81,7 +84,7 @@ export async function fetchIpLocation() { /** * Crée un nouveau post - * token: access token Keycloak (string) + * token: access token Keycloak (string) ou null */ export async function createPost(payload, token) { const headers = { @@ -108,11 +111,12 @@ export async function createPost(payload, token) { } /** - * Crée un nouvel utilisateur (backend doit exposer POST /api/register) + * Crée un nouvel utilisateur via le backend + * (backend: POST /api/signup) * payload: { username, email, password } */ export async function registerUser(payload) { - const res = await fetch(`${API_BASE}/register`, { + const res = await fetch(`${API_BASE}/signup`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/src/auth/AuthContext.jsx b/src/auth/AuthContext.jsx index d2689ca..86c8c49 100644 --- a/src/auth/AuthContext.jsx +++ b/src/auth/AuthContext.jsx @@ -1,73 +1,62 @@ -import React, { createContext, useContext, useEffect, useState } from "react"; +import React, { + createContext, + useContext, + useEffect, + useState, +} from "react"; -// CONFIG KEYCLOAK +const AuthContext = createContext(null); + +// Config Keycloak pour le login inline const KC_BASE = "https://web.sociowire.com:8443"; const KC_REALM = "sociowire"; const KC_CLIENT_ID = "sociowire-frontend"; -const TOKEN_KEY = "sociowire:token"; - -const AuthContext = createContext(null); - -function parseJwt(token) { - try { - const [, payload] = token.split("."); - const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/")); - return JSON.parse(json); - } catch { - return {}; - } -} +const TOKEN_KEY = "sociowire:access_token"; +const USERNAME_KEY = "sociowire:username"; export function AuthProvider({ children }) { - const [token, setToken] = useState(null); + const [loading, setLoading] = useState(true); const [authenticated, setAuthenticated] = useState(false); const [username, setUsername] = useState(""); - const [loading, setLoading] = useState(true); + const [token, setToken] = useState(null); const [lastError, setLastError] = useState(""); - // charger token depuis localStorage au démarrage + // Charge token/username depuis localStorage au démarrage useEffect(() => { try { - const saved = localStorage.getItem(TOKEN_KEY); - if (saved) { - const data = JSON.parse(saved); - if (data && data.access_token) { - const now = Date.now() / 1000; - const payload = parseJwt(data.access_token); - if (!payload.exp || payload.exp > now) { - setToken(data.access_token); - setAuthenticated(true); - const name = - payload.preferred_username || - payload.email || - payload.name || - (payload.sub ? String(payload.sub) : ""); - setUsername(name || ""); - } - } + const storedToken = localStorage.getItem(TOKEN_KEY); + const storedUser = localStorage.getItem(USERNAME_KEY) || ""; + if (storedToken) { + setToken(storedToken); + setUsername(storedUser); + setAuthenticated(true); + } else { + setAuthenticated(false); } } catch (e) { - console.warn("[AUTH] load token error", e); + console.warn("[AUTH] localStorage error", e); } finally { setLoading(false); } }, []); + // Login via password grant sur Keycloak async function login(user, pass) { setLastError(""); - if (!user || !pass) { - setLastError("Username & password required."); + const u = (user || "").trim(); + if (!u || !pass) { + setLastError("Username and password required."); return; } - setLoading(true); - try { - const params = new URLSearchParams(); - params.set("grant_type", "password"); - params.set("client_id", KC_CLIENT_ID); - params.set("username", user); - params.set("password", pass); + 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 { const res = await fetch( `${KC_BASE}/realms/${KC_REALM}/protocol/openid-connect/token`, { @@ -82,72 +71,96 @@ export function AuthProvider({ children }) { if (!res.ok) { const text = await res.text(); console.error("[AUTH] login failed", res.status, text); - setLastError("Invalid credentials or auth error."); + + let msg = "Invalid credentials or auth error."; + try { + const j = JSON.parse(text); + if (j.error_description) { + msg = j.error_description; + } else if (j.error) { + msg = j.error; + } + } catch (e) { + // ignore parse error + } + + setLastError(`${res.status} ${msg}`); setAuthenticated(false); setToken(null); setUsername(""); try { localStorage.removeItem(TOKEN_KEY); - } catch {} + localStorage.removeItem(USERNAME_KEY); + } catch (e) { + // ignore + } return; } const data = await res.json(); const accessToken = data.access_token; if (!accessToken) { - setLastError("No access token returned."); + setLastError("No access token in response."); + setAuthenticated(false); + setToken(null); + setUsername(""); return; } - const payload = parseJwt(accessToken); - const name = - payload.preferred_username || - payload.email || - payload.name || - (payload.sub ? String(payload.sub) : ""); - setToken(accessToken); setAuthenticated(true); - setUsername(name || ""); + setUsername(u); setLastError(""); try { - localStorage.setItem(TOKEN_KEY, JSON.stringify(data)); + localStorage.setItem(TOKEN_KEY, accessToken); + localStorage.setItem(USERNAME_KEY, u); } catch (e) { - console.warn("[AUTH] save token error", e); + console.warn("[AUTH] failed to persist token", e); } - } catch (err) { - console.error("[AUTH] login error", err); - setLastError("Network/auth error."); + } catch (e) { + console.error("[AUTH] network error", e); + setLastError("Network error during login."); setAuthenticated(false); setToken(null); setUsername(""); - } finally { - setLoading(false); + try { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(USERNAME_KEY); + } catch (e2) { + // ignore + } } } function logout() { - setToken(null); setAuthenticated(false); + setToken(null); setUsername(""); setLastError(""); try { localStorage.removeItem(TOKEN_KEY); - } catch {} + localStorage.removeItem(USERNAME_KEY); + } catch (e) { + // ignore + } } - const value = { - loading, - authenticated, - username, - token, - lastError, - login, - logout, - }; - - return {children}; + return ( + + {children} + + ); } export function useAuth() { diff --git a/src/components/Layout/TopBar.jsx b/src/components/Layout/TopBar.jsx index 92852cd..0c00857 100644 --- a/src/components/Layout/TopBar.jsx +++ b/src/components/Layout/TopBar.jsx @@ -18,13 +18,16 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { lastError, } = useAuth(); - const [mode, setMode] = useState("login"); // "login" | "register" + const [showAuth, setShowAuth] = useState(false); + const [mode, setMode] = useState("login"); // "login" | "signup" - // login form - const [user, setUser] = useState(""); - const [pass, setPass] = useState(""); + // login + const [loginUser, setLoginUser] = useState(""); + const [loginPass, setLoginPass] = useState(""); - // signup form + // signup + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); const [regUser, setRegUser] = useState(""); const [regEmail, setRegEmail] = useState(""); const [regPass, setRegPass] = useState(""); @@ -35,16 +38,28 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { if (loading) authLabel = "AUTH: loading…"; else if (authenticated) authLabel = `AUTH: user=${username || "?"}`; - const handleLoginSubmit = (e) => { - e.preventDefault(); - login(user.trim(), pass); + const openModal = (nextMode) => { + setMode(nextMode); + setShowAuth(true); + setSignupError(""); }; - const handleRegisterSubmit = async (e) => { + const closeModal = () => { + setShowAuth(false); + }; + + const handleLoginSubmit = async (e) => { + e.preventDefault(); + await login(loginUser, loginPass); + // on laisse l'utilisateur fermer manuellement, + // il voit en haut si ça a marché ou pas. + }; + + const handleSignupSubmit = async (e) => { e.preventDefault(); setSignupError(""); - if (!regUser.trim() || !regEmail.trim() || !regPass) { - setSignupError("All fields required."); + if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) { + setSignupError("Nom, prénom, user, email et mot de passe sont requis."); return; } try { @@ -53,9 +68,13 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { username: regUser.trim(), email: regEmail.trim(), password: regPass, + firstName: firstName.trim(), + lastName: lastName.trim(), }); - // auto login après création - login(regUser.trim(), regPass); + // après création, on tente le login direct + await login(regUser.trim(), regPass); + // on ferme le popup, l'état auth s'affiche dans la topbar + setShowAuth(false); } catch (err) { console.error(err); setSignupError(err.message || "Register error."); @@ -65,215 +84,192 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { }; return ( -
-
SW
- -
-
SOCIOWIRE.com
-
Wired to life
-
- {authLabel} - {lastError && ( - - ({lastError}) - - )} - {signupError && ( - - ({signupError}) - - )} -
-
- -
-
- {["dark", "blue", "light"].map((t) => ( - - ))} -
- - {authenticated ? ( -
- {username} - + <> +
+
+
SW
+
+
SOCIOWIRE.com
+
Wired to life
+
+ {authLabel} + {lastError && ( + + ({lastError}) + + )} +
- ) : ( -
-
+
+ +
+
+ {["dark", "blue", "light"].map((t) => ( + + ))} +
+ + {authenticated ? ( +
+ {username} + +
+ ) : ( +
+ )} +
+
+ + {showAuth && ( +
+
e.stopPropagation()} + > +
+ + + +
{mode === "login" ? ( -
- setUser(e.target.value)} - style={{ - fontSize: "0.7rem", - padding: "0.15rem 0.4rem", - borderRadius: 999, - border: "1px solid #4b5563", - background: "#020617", - color: "#e5e7eb", - }} - /> - setPass(e.target.value)} - style={{ - fontSize: "0.7rem", - padding: "0.15rem 0.4rem", - borderRadius: 999, - border: "1px solid #4b5563", - background: "#020617", - color: "#e5e7eb", - }} - /> + + +
) : ( -
- setRegUser(e.target.value)} - style={{ - fontSize: "0.7rem", - padding: "0.15rem 0.4rem", - borderRadius: 999, - border: "1px solid #4b5563", - background: "#020617", - color: "#e5e7eb", - minWidth: "70px", - }} - /> - setRegEmail(e.target.value)} - style={{ - fontSize: "0.7rem", - padding: "0.15rem 0.4rem", - borderRadius: 999, - border: "1px solid #4b5563", - background: "#020617", - color: "#e5e7eb", - minWidth: "90px", - }} - /> - setRegPass(e.target.value)} - style={{ - fontSize: "0.7rem", - padding: "0.15rem 0.4rem", - borderRadius: 999, - border: "1px solid #4b5563", - background: "#020617", - color: "#e5e7eb", - minWidth: "70px", - }} - /> + +
+ + +
+ + + + + {signupError && ( +
{signupError}
+ )} +
)}
- )} -
-
+ + )} + ); } diff --git a/src/components/Map/useUserPosition.js b/src/components/Map/useUserPosition.js index 5cff856..892da1a 100644 --- a/src/components/Map/useUserPosition.js +++ b/src/components/Map/useUserPosition.js @@ -10,14 +10,16 @@ export function useUserPosition(mapRef, hasLastView) { if (!map) return; let cancelled = false; + let hasPos = false; function saveUserPos(lat, lon) { if (cancelled) return; + hasPos = true; const coords = [lon, lat]; setUserPosition(coords); try { localStorage.setItem(LAST_POS_KEY, JSON.stringify({ lat, lon })); - } catch { + } catch (e) { // ignore } @@ -32,27 +34,27 @@ export function useUserPosition(mapRef, hasLastView) { } } - // 1) dernière position user + // 1) dernière position user dans localStorage try { const raw = localStorage.getItem(LAST_POS_KEY); if (raw) { const p = JSON.parse(raw); if (typeof p.lat === "number" && typeof p.lon === "number") { + hasPos = true; setUserPosition([p.lon, p.lat]); } } - } catch { + } catch (e) { // ignore } - // 2) GPS → 3) IP backend - function viaIpFallback() { - (async () => { - const ipCoords = await fetchIpLocation(); - if (!ipCoords || cancelled) return; - const [lon, lat] = ipCoords; - saveUserPos(lat, lon); - })(); + // 2) GPS → 3) IP backend (uniquement si on n'a VRAIMENT rien) + async function viaIpFallback() { + if (cancelled || hasPos) return; + const ipCoords = await fetchIpLocation(); + if (!ipCoords || cancelled) return; + const [lon, lat] = ipCoords; + saveUserPos(lat, lon); } if ("geolocation" in navigator) { @@ -62,7 +64,7 @@ export function useUserPosition(mapRef, hasLastView) { saveUserPos(pos.coords.latitude, pos.coords.longitude); }, () => { - if (cancelled) return; + if (cancelled || hasPos) return; viaIpFallback(); }, { @@ -71,7 +73,8 @@ export function useUserPosition(mapRef, hasLastView) { maximumAge: 30000, } ); - } else { + } else if (!hasPos) { + // pas de geolocation et aucune position connue → IP fallback viaIpFallback(); } diff --git a/src/main.jsx b/src/main.jsx index d7caeed..afc3720 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -11,7 +11,8 @@ import "./styles/popup.css"; import "./styles/overlays.css"; import "./styles/posts.css"; import "./styles/theme.css"; - +// dans main.jsx, avec les autres styles +import "./styles/auth-modal.css"; import { AuthProvider } from "./auth/AuthContext.jsx"; ReactDOM.createRoot(document.getElementById("root")).render( diff --git a/src/styles/auth-modal.css b/src/styles/auth-modal.css new file mode 100644 index 0000000..8b5b478 --- /dev/null +++ b/src/styles/auth-modal.css @@ -0,0 +1,89 @@ +.auth-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 50; +} + +.auth-modal { + width: 100%; + max-width: 360px; + background: #020617; + border-radius: 16px; + border: 1px solid rgba(148, 163, 184, 0.9); + box-shadow: + 0 18px 45px rgba(0, 0, 0, 0.9), + 0 0 24px rgba(56, 189, 248, 0.5); + padding: 0.9rem 1rem 1rem 1rem; + color: #e5e7eb; +} + +.auth-modal-header { + display: flex; + align-items: center; + gap: 0.4rem; + margin-bottom: 0.7rem; +} + +.auth-tab { + border-radius: 999px; + border: 1px solid #4b5563; + background: #020617; + color: #e5e7eb; + font-size: 0.75rem; + padding: 0.25rem 0.7rem; + cursor: pointer; +} + +.auth-tab-active { + background: #1e293b; + border-color: #60a5fa; +} + +.auth-close { + margin-left: auto; + border: none; + background: transparent; + color: #9ca3af; + cursor: pointer; + font-size: 0.9rem; +} + +.auth-form { + display: flex; + flex-direction: column; + gap: 0.45rem; +} + +.auth-form label { + display: flex; + flex-direction: column; + font-size: 0.75rem; + gap: 0.15rem; +} + +.auth-form input { + font-size: 0.8rem; + padding: 0.25rem 0.55rem; + border-radius: 999px; + border: 1px solid #4b5563; + background: #020617; + color: #e5e7eb; +} + +.auth-row { + display: flex; + gap: 0.4rem; +} + +.auth-row label { + flex: 1; +} + +.auth-error { + font-size: 0.7rem; + color: #fecaca; +} diff --git a/src/styles/topbar.css b/src/styles/topbar.css index 4b8848f..939e180 100644 --- a/src/styles/topbar.css +++ b/src/styles/topbar.css @@ -2,6 +2,7 @@ .topbar { display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.6rem; @@ -24,6 +25,7 @@ .title-block { display: flex; flex-direction: column; + min-width: 0; } .main-title { @@ -36,14 +38,16 @@ opacity: 0.7; } -/* bloc de droite : thèmes + bouton login */ +/* bloc de droite : thèmes + boutons */ .topbar-right { display: flex; + flex-wrap: wrap; align-items: center; + justify-content: flex-end; gap: 0.5rem; + margin-left: auto; } -/* petits boutons de thème (D / B / L) */ .theme-switch { display: flex; gap: 0.25rem; @@ -69,7 +73,6 @@ box-shadow: 0 0 10px rgba(250, 204, 21, 0.7); } -/* bouton Login */ .btn-primary { padding: 0.25rem 0.7rem; font-size: 0.75rem; @@ -79,3 +82,25 @@ color: #f9fafb; cursor: pointer; } + +.btn-secondary { + padding: 0.25rem 0.7rem; + font-size: 0.75rem; + border-radius: 999px; + border: 1px solid #60a5fa; + background: #1e293b; + color: #dbeafe; + cursor: pointer; +} + +.btn-full { + width: 100%; +} + +/* mobile */ +@media (max-width: 480px) { + .topbar-right { + width: 100%; + justify-content: flex-start; + } +}