tout fonctionne logins et post correct
This commit is contained in:
parent
48d5ed01a0
commit
d2bde41aa4
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<TopBar theme={theme} onChangeTheme={setTheme} />
|
||||
<div className="main-shell">
|
||||
<div className="map-shell">
|
||||
{/* on passe le thème à la carte */}
|
||||
<Suspense fallback={<div style={{ padding: 16 }}>Loading map…</div>}>
|
||||
<MapView theme={theme} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
keycloak: kc,
|
||||
loading,
|
||||
authenticated,
|
||||
username,
|
||||
login,
|
||||
logout,
|
||||
lastError,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useAuth must be used inside <AuthProvider>");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 (
|
||||
<header className="topbar">
|
||||
<div className="logo-circle">SW</div>
|
||||
|
|
@ -14,6 +21,14 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
|
|||
<div className="title-block">
|
||||
<div className="main-title">SOCIOWIRE.com</div>
|
||||
<div className="sub-title">Wired to life</div>
|
||||
<div style={{ fontSize: "0.65rem", opacity: 0.7 }}>
|
||||
{authLabel}
|
||||
{lastError && (
|
||||
<span style={{ color: "#f97373", marginLeft: "0.3rem" }}>
|
||||
(auth err)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="topbar-right">
|
||||
|
|
@ -32,7 +47,23 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
|
|||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button className="btn-primary">Login</button>
|
||||
|
||||
{loading ? (
|
||||
<button className="btn-primary" disabled>
|
||||
…
|
||||
</button>
|
||||
) : authenticated ? (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
|
||||
<span style={{ fontSize: "0.8rem" }}>{username}</span>
|
||||
<button className="btn-primary" onClick={logout}>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button className="btn-primary" onClick={login}>
|
||||
Login
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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" }) {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* CREATE POST */}
|
||||
{/* CROSSHAIR au centre pendant la création */}
|
||||
{isCreating && (
|
||||
<div className="map-overlay map-crosshair">
|
||||
<div className="crosshair-aim" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CREATE POST PANEL */}
|
||||
{isCreating && (
|
||||
<div className="map-overlay create-post-panel">
|
||||
<div className="create-post-header">
|
||||
|
|
@ -246,7 +311,8 @@ export default function MapView({ theme = "dark" }) {
|
|||
</button>
|
||||
</div>
|
||||
<span className="crosshair-label">
|
||||
Move the map, then tap to fix the position.
|
||||
Move the map, aim with the crosshair — your wire will be pinned
|
||||
there.
|
||||
</span>
|
||||
<div className="create-row">
|
||||
<select
|
||||
|
|
@ -293,7 +359,7 @@ export default function MapView({ theme = "dark" }) {
|
|||
onClick={handleSubmitPost}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? "Posting..." : "Post as tommy"}
|
||||
{isSaving ? "Posting..." : "Post"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,8 +12,12 @@ import "./styles/overlays.css";
|
|||
import "./styles/posts.css";
|
||||
import "./styles/theme.css";
|
||||
|
||||
import { AuthProvider } from "./auth/AuthContext.jsx";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue