From d2bde41aa4668bc05334514b82ca11e91fe8495f Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 10 Dec 2025 19:45:06 +0000 Subject: [PATCH] tout fonctionne logins et post correct --- deploy.sh | 0 package-lock.json | 10 +++ package.json | 1 + src/App.jsx | 11 +-- src/api/client.js | 28 +++----- src/auth/AuthContext.jsx | 111 +++++++++++++++++++++++++++++++ src/auth/authUtils.js | 57 ++++++++++++++++ src/auth/keycloak.js | 9 +++ src/components/Layout/TopBar.jsx | 33 ++++++++- src/components/Map/MapView.jsx | 82 ++++++++++++++++++++--- src/main.jsx | 6 +- 11 files changed, 316 insertions(+), 32 deletions(-) mode change 100644 => 100755 deploy.sh create mode 100644 src/auth/AuthContext.jsx create mode 100644 src/auth/authUtils.js create mode 100644 src/auth/keycloak.js diff --git a/deploy.sh b/deploy.sh old mode 100644 new mode 100755 diff --git a/package-lock.json b/package-lock.json index 34e1992..5c72180 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "axios": "^1.13.2", + "keycloak-js": "^26.2.1", "maplibre-gl": "^5.14.0", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -2200,6 +2201,15 @@ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", "license": "ISC" }, + "node_modules/keycloak-js": { + "version": "26.2.1", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.1.tgz", + "integrity": "sha512-bZt6fQj/TLBAmivXSxSlqAJxBx/knNZDQGJIW4ensGYGN4N6tUKV8Zj3Y7/LOV8eIpvWsvqV70fbACihK8Ze0Q==", + "license": "Apache-2.0", + "workspaces": [ + "test" + ] + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index e6a6af5..a247088 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "axios": "^1.13.2", + "keycloak-js": "^26.2.1", "maplibre-gl": "^5.14.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/src/App.jsx b/src/App.jsx index dd4df67..752fd29 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,8 @@ -import React, { useEffect, useState } from "react"; +import React, { Suspense, useEffect, useState } from "react"; import TopBar from "./components/Layout/TopBar"; -import MapView from "./components/Map/MapView"; + +// Lazy-load de la carte (gros chunk : maplibre, CSS, etc.) +const MapView = React.lazy(() => import("./components/Map/MapView")); const THEME_KEY = "sociowire:theme"; @@ -39,8 +41,9 @@ export default function App() {
- {/* on passe le thème à la carte */} - + Loading map…
}> + +
diff --git a/src/api/client.js b/src/api/client.js index d1f5325..efd5202 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -4,17 +4,6 @@ const API_BASE = "/api"; -/** - * Récupère la liste des posts avec filtres. - * filters: { - * category?: "NEWS" | "FRIENDS" | "EVENT" | "MARKET" - * subCategory?: string - * time?: "NOW" | "TODAY" | "RECENT" | "PAST" - * lat?: number - * lon?: number - * radiusKm?: number - * } - */ export async function fetchPosts(filters = {}) { const params = new URLSearchParams(); @@ -45,7 +34,6 @@ export async function fetchPosts(filters = {}) { /** * Récupère la position approx de l'utilisateur depuis le backend. - * Backend: GET /api/ip-location → { lat: number, lon: number } ou "null" */ export async function fetchIpLocation() { try { @@ -93,15 +81,19 @@ export async function fetchIpLocation() { /** * Crée un nouveau post - * Backend: POST /api/post (postCreateHandler) - * Attend: { title, snippet, category, sub_category, lat, lon, author } + * token: access token Keycloak (string) */ -export async function createPost(payload) { +export async function createPost(payload, token) { + const headers = { + "Content-Type": "application/json", + }; + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + const res = await fetch(`${API_BASE}/post`, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers, credentials: "include", body: JSON.stringify(payload), }); diff --git a/src/auth/AuthContext.jsx b/src/auth/AuthContext.jsx new file mode 100644 index 0000000..0c2d9d6 --- /dev/null +++ b/src/auth/AuthContext.jsx @@ -0,0 +1,111 @@ +import React, { createContext, useContext, useEffect, useState } from "react"; +import keycloak from "./keycloak"; + +const AuthContext = createContext(null); + +export function AuthProvider({ children }) { + const [kc, setKc] = useState(null); + const [loading, setLoading] = useState(true); + const [authenticated, setAuthenticated] = useState(false); + const [username, setUsername] = useState(""); + const [lastError, setLastError] = useState(""); + + useEffect(() => { + let isMounted = true; + + async function init() { + try { + console.log("[AUTH] init keycloak…"); + const ok = await keycloak.init({ + onLoad: "check-sso", // ne force pas le login au chargement + checkLoginIframe: false, + }); + + if (!isMounted) return; + + console.log("[AUTH] init result:", ok); + window.kc = keycloak; // debug global + + setKc(keycloak); + setAuthenticated(ok); + + if (ok && keycloak.tokenParsed) { + const p = keycloak.tokenParsed; + const name = + p.preferred_username || + p.email || + p.name || + (p.sub ? String(p.sub) : ""); + setUsername(name || ""); + } else { + setUsername(""); + } + } catch (err) { + console.error("[AUTH] keycloak init error", err); + if (isMounted) { + setLastError(String(err)); + } + } finally { + if (isMounted) setLoading(false); + } + } + + init(); + + // refresh token simplifié + const interval = setInterval(() => { + if (!keycloak || !keycloak.authenticated) return; + keycloak + .updateToken(60) + .then((refreshed) => { + if (refreshed) { + console.log("[AUTH] token refreshed"); + } + }) + .catch((err) => { + console.warn("[AUTH] token refresh error", err); + }); + }, 30000); + + return () => { + isMounted = false; + clearInterval(interval); + }; + }, []); + + const login = () => { + keycloak.login({ + redirectUri: window.location.href, + }); + }; + + const logout = () => { + keycloak.logout({ + redirectUri: window.location.origin + "/", + }); + }; + + 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/authUtils.js b/src/auth/authUtils.js new file mode 100644 index 0000000..a908834 --- /dev/null +++ b/src/auth/authUtils.js @@ -0,0 +1,57 @@ +import keycloak from "./keycloak"; + +/** + * Initialise Keycloak sans forcer le login. + * - onLoad: "check-sso" => ne redirige pas si user non loggé + */ +export function initAuth() { + return keycloak.init({ + onLoad: "check-sso", + checkLoginIframe: false, + }); +} + +/** + * Appelle une action qui nécessite d'être authentifié. + * Si non loggé => ouvre la page de login Keycloak, puis revient sur la même page. + */ +export function requireAuth(action) { + if (keycloak.authenticated) { + return action(); + } + + keycloak.login({ + redirectUri: window.location.href, + }); +} + +/** + * Retourne le token JWT actuel (ou null) + */ +export function getToken() { + return keycloak.token || null; +} + +/** + * Retourne le username depuis le token (si présent) + */ +export function getUsername() { + const profile = keycloak.tokenParsed || {}; + return ( + profile.preferred_username || + profile.email || + profile.name || + null + ); +} + +/** + * Logout et revient sur la même page. + */ +export function logout() { + keycloak.logout({ + redirectUri: window.location.origin, + }); +} + +export default keycloak; diff --git a/src/auth/keycloak.js b/src/auth/keycloak.js new file mode 100644 index 0000000..30afe2f --- /dev/null +++ b/src/auth/keycloak.js @@ -0,0 +1,9 @@ +import Keycloak from "keycloak-js"; + +const keycloak = new Keycloak({ + url: "https://web.sociowire.com:8443", + realm: "sociowire", + clientId: "sociowire-frontend", +}); + +export default keycloak; diff --git a/src/components/Layout/TopBar.jsx b/src/components/Layout/TopBar.jsx index 56e11a6..f733643 100644 --- a/src/components/Layout/TopBar.jsx +++ b/src/components/Layout/TopBar.jsx @@ -1,4 +1,5 @@ import React from "react"; +import { useAuth } from "../../auth/AuthContext"; const THEME_LABELS = { dark: "Dark", @@ -7,6 +8,12 @@ const THEME_LABELS = { }; export default function TopBar({ theme = "dark", onChangeTheme }) { + const { loading, authenticated, username, login, logout, lastError } = useAuth(); + + let authLabel = "AUTH: anon"; + if (loading) authLabel = "AUTH: loading…"; + else if (authenticated) authLabel = `AUTH: user=${username || "?"}`; + return (
SW
@@ -14,6 +21,14 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
SOCIOWIRE.com
Wired to life
+
+ {authLabel} + {lastError && ( + + (auth err) + + )} +
@@ -32,7 +47,23 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { ))}
- + + {loading ? ( + + ) : authenticated ? ( +
+ {username} + +
+ ) : ( + + )}
); diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx index 62c3b82..4d98ca4 100644 --- a/src/components/Map/MapView.jsx +++ b/src/components/Map/MapView.jsx @@ -14,8 +14,11 @@ import { useMapCore } from "./useMapCore"; import { useUserPosition } from "./useUserPosition"; import { usePostsEngine } from "./usePostsEngine"; import PostList from "../Posts/PostList"; +import { useAuth } from "../../auth/AuthContext"; export default function MapView({ theme = "dark" }) { + const { authenticated, username } = useAuth(); + const { containerRef, mapRef, @@ -69,6 +72,7 @@ export default function MapView({ theme = "dark" }) { } }, [mainFilter, bottomCategories, subFilter]); + // WebSocket live useEffect(() => { const proto = window.location.protocol === "https:" ? "wss" : "ws"; const host = @@ -102,6 +106,10 @@ export default function MapView({ theme = "dark" }) { }; const handleOpenCreate = () => { + if (!authenticated) { + alert("You must be logged in to place a wire."); + return; + } setIsCreating(true); setDraftTitle(""); setDraftBody(""); @@ -121,26 +129,76 @@ export default function MapView({ theme = "dark" }) { setSaveError(""); const map = mapRef.current; if (!map) return; - let coords = draftCoords || [map.getCenter().lng, map.getCenter().lat]; - const [lng, lat] = coords; + + if (!authenticated) { + setSaveError("You must be logged in to post."); + return; + } + + const authorName = (username || "").trim() || "anon"; + + // ----- CROSSHAIR OFFSET FIX ----- + // Point de départ : soit coords définies, soit centre de la map + let baseLng; + let baseLat; + + if (draftCoords && draftCoords.length === 2) { + baseLng = draftCoords[0]; + baseLat = draftCoords[1]; + } else { + const center = map.getCenter(); + baseLng = center.lng; + baseLat = center.lat; + } + + // Offset vertical en pixels : + // si le post apparaît trop bas, on remonte (pixelOffsetY négatif) + const pixelOffsetY = -20; // ajuste à -25/-30 si nécessaire + + // lng/lat -> pixels + const screenPoint = map.project([baseLng, baseLat]); + + // appliquer l'offset vertical + const correctedPoint = { + x: screenPoint.x, + y: screenPoint.y + pixelOffsetY, + }; + + // pixels -> lng/lat corrigé + const correctedLngLat = map.unproject([correctedPoint.x, correctedPoint.y]); + const lng = correctedLngLat.lng; + const lat = correctedLngLat.lat; + // ----- END FIX ----- + if (!draftTitle.trim()) { setSaveError("Title required."); return; } + try { setIsSaving(true); - await createPost({ + + // On récupère le post retourné par le backend + const newPost = await createPost({ title: draftTitle.trim(), snippet: draftBody.trim() || draftTitle.trim(), category: draftCategory.toUpperCase(), sub_category: draftSubCategory || "ALL", lat, lon: lng, - author: "tommy", + author: authorName, }); + + // On l’injecte direct dans le moteur de posts, + // en plus du broadcast WebSocket + if (newPost && newPost.id) { + handleIncomingPost(newPost); + } + setIsSaving(false); setIsCreating(false); - } catch { + } catch (e) { + console.error(e); setIsSaving(false); setSaveError("Error creating post."); } @@ -233,7 +291,14 @@ export default function MapView({ theme = "dark" }) { - {/* CREATE POST */} + {/* CROSSHAIR au centre pendant la création */} + {isCreating && ( +
+
+
+ )} + + {/* CREATE POST PANEL */} {isCreating && (
@@ -246,7 +311,8 @@ export default function MapView({ theme = "dark" }) {
- Move the map, then tap to fix the position. + Move the map, aim with the crosshair — your wire will be pinned + there.