diff --git a/backend/src/controllers/lpc_controller.rs b/backend/src/controllers/lpc_controller.rs index 985c2360..9e99b5da 100644 --- a/backend/src/controllers/lpc_controller.rs +++ b/backend/src/controllers/lpc_controller.rs @@ -3,19 +3,24 @@ use axum::Json; use sqlx::MySqlPool; use crate::service::monitored_website_service::MonitoredWebsiteService; use crate::dto::lpc_dto::LastPageConsultedInfos; +use crate::dto::lpc_dto::LastPageConsultedQuery; use crate::dto::lpc_dto::LastPageConsultedResponse; -use crate::middleware::auth::AuthenticatedUser; +use tower_sessions::Session; +use crate::dto::user_full::UserFull; use crate::error::AppError; pub async fn lpc( State(pool): State, - AuthenticatedUser(user_full): AuthenticatedUser, - Query(params): Query, + session: Session, + Query(params): Query, ) -> Result, AppError> { - let user_id = Some(user_full.user.id); + let user_id: Option = session.get("user_full").await + .ok() + .and_then(|user_full: Option| user_full.map(|u| u.user.id)); + let infos: Option = params.into_infos(); - let response = MonitoredWebsiteService::lpc(&pool, user_id, Some(params)).await + let response = MonitoredWebsiteService::lpc(&pool, user_id, infos).await .map_err(AppError::from)?; Ok(Json(response)) diff --git a/backend/src/dto/lpc_dto.rs b/backend/src/dto/lpc_dto.rs index d182a888..89c7e392 100644 --- a/backend/src/dto/lpc_dto.rs +++ b/backend/src/dto/lpc_dto.rs @@ -1,5 +1,6 @@ use crate::models::equivalent::Equivalent; use serde::{Deserialize, Serialize}; + #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow, PartialEq)] pub struct LastPageConsultedInfos { pub url_full: String, @@ -10,6 +11,29 @@ pub struct LastPageConsultedInfos { pub country: String, } +#[derive(Debug, Deserialize, Clone, Default, PartialEq)] +pub struct LastPageConsultedQuery { + pub url_full: Option, + pub queries_quantity: Option, + pub carbon_footprint: Option, + pub data_transferred: Option, + pub loading_time: Option, + pub country: Option, +} + +impl LastPageConsultedQuery { + pub fn into_infos(self) -> Option { + Some(LastPageConsultedInfos { + url_full: self.url_full?, + queries_quantity: self.queries_quantity?, + carbon_footprint: self.carbon_footprint?, + data_transferred: self.data_transferred?, + loading_time: self.loading_time?, + country: self.country?, + }) + } +} + #[derive(Debug, Serialize, PartialEq)] pub struct LastPageConsultedResponse { pub success: bool, diff --git a/backend/src/main.rs b/backend/src/main.rs index fffea3a4..fa8fd312 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -28,7 +28,7 @@ async fn main() { .with_http_only(true) .with_same_site(tower_sessions::cookie::SameSite::Lax) .with_name("greenscoreweb_sessions") - .with_expiry(Expiry::OnInactivity(Duration::seconds(3600))); + .with_expiry(Expiry::OnInactivity(Duration::days(30))); let cors = CorsLayer::new() .allow_origin([ diff --git a/backend/tests/lpc_controller_tests.rs b/backend/tests/lpc_controller_tests.rs index d29c9b9e..1502c24f 100644 --- a/backend/tests/lpc_controller_tests.rs +++ b/backend/tests/lpc_controller_tests.rs @@ -1,10 +1,11 @@ use backend::controllers::lpc_controller; -use backend::dto::lpc_dto::{LastPageConsultedInfos}; +use backend::dto::lpc_dto::{LastPageConsultedInfos, LastPageConsultedQuery}; use backend::dto::user_full::UserFull; use backend::models::user::User; -use backend::middleware::auth::AuthenticatedUser; use axum::extract::{Query, State}; use sqlx::{MySqlPool}; +use std::sync::Arc; +use tower_sessions::{MemoryStore, Session}; fn create_dummy_user_full(id: i64) -> UserFull { UserFull { @@ -24,7 +25,8 @@ fn create_dummy_user_full(id: i64) -> UserFull { #[sqlx::test] async fn devrait_retourner_succes_et_enregistrer_donnees_pour_lpc(pool: MySqlPool) { // GIVEN - let authenticated_user = AuthenticatedUser(create_dummy_user_full(1)); + let session = Session::new(None, Arc::new(MemoryStore::default()), None); + session.insert("user_full", create_dummy_user_full(1)).await.unwrap(); let params = LastPageConsultedInfos { url_full: "https://example.com/page".to_string(), @@ -38,8 +40,15 @@ async fn devrait_retourner_succes_et_enregistrer_donnees_pour_lpc(pool: MySqlPoo // WHEN let result = lpc_controller::lpc( State(pool.clone()), - authenticated_user, - Query(params.clone()), + session, + Query(LastPageConsultedQuery { + url_full: Some(params.url_full.clone()), + queries_quantity: Some(params.queries_quantity), + carbon_footprint: Some(params.carbon_footprint), + data_transferred: Some(params.data_transferred), + loading_time: Some(params.loading_time), + country: Some(params.country.clone()), + }), ).await; // THEN @@ -64,3 +73,22 @@ async fn devrait_retourner_succes_et_enregistrer_donnees_pour_lpc(pool: MySqlPoo .unwrap_or(0); assert_eq!(count, 1, "Devrait avoir inséré un enregistrement dans monitored_websites"); } + +#[sqlx::test] +async fn ne_doit_pas_retourner_400_quand_la_query_est_absente(pool: MySqlPool) { + // GIVEN + let session = Session::new(None, Arc::new(MemoryStore::default()), None); + session.insert("user_full", create_dummy_user_full(1)).await.unwrap(); + + // WHEN + let result = lpc_controller::lpc( + State(pool), + session, + Query(LastPageConsultedQuery::default()), + ).await; + + // THEN + assert!(result.is_ok(), "Le contrôleur devrait accepter une query absente"); + let response = result.unwrap().0; + assert!(response.success, "La réponse devrait indiquer un succès"); +} diff --git a/frontend/src/routes/(app)/derniere-page-consultee/+page.server.ts b/frontend/src/routes/(app)/derniere-page-consultee/+page.server.ts index d46073c3..98e725a7 100644 --- a/frontend/src/routes/(app)/derniere-page-consultee/+page.server.ts +++ b/frontend/src/routes/(app)/derniere-page-consultee/+page.server.ts @@ -1,8 +1,18 @@ import type { PageServerLoad } from './$types'; import { BACKEND_URL, ELECTRICITY_MAP_API_KEY } from "$lib/config.ts"; +const emptyPagePayload = { + pageData: null, + adviceUser: '', + adviceDev: '', + letterGreenScore: '', + envNomination: '', + equivalents: [] +}; + export const load: PageServerLoad = async ({ fetch, request, url }) => { try { + let backendUrl = `${BACKEND_URL}/derniere-page-consultee`; if (url.searchParams.toString()) { @@ -17,17 +27,22 @@ export const load: PageServerLoad = async ({ fetch, request, url }) => { }, credentials: 'include' }); - const result = await response.json(); + let result; + if (!response.ok) { + const errorText = await response.text(); + console.error('Réponse backend non-OK:', response.status, errorText); + return emptyPagePayload; + } + try { + result = await response.json(); + } catch (jsonError) { + const errorText = await response.text(); + console.error('Réponse backend non JSON:', jsonError, errorText); + return emptyPagePayload; + } if (!result.success) { - return { - pageData: null, - adviceUser: '', - adviceDev: '', - letterGreenScore: '', - envNomination: '', - equivalents: [] - }; + return emptyPagePayload; } // Récupérer le code ISO et le drapeau @@ -70,7 +85,7 @@ export const load: PageServerLoad = async ({ fetch, request, url }) => { return { pageData: { - link: result.lpc_infos?.link || null, + link: result.lpc_infos?.url_full || null, letterGreenScore: 'A', country: country || 'Inconnu', carbonFootprint: result.lpc_infos?.carbon_footprint || 0, @@ -88,13 +103,6 @@ export const load: PageServerLoad = async ({ fetch, request, url }) => { }; } catch (error) { console.error('Erreur lors de la récupération des données :', error); - return { - pageData: null, - adviceUser: '', - adviceDev: '', - letterGreenScore: '', - envNomination: '', - equivalents: [] - }; + return emptyPagePayload; } } diff --git a/plugin/chrome/_locales/en/messages.json b/plugin/chrome/_locales/en/messages.json index 7abf7668..3697e198 100644 --- a/plugin/chrome/_locales/en/messages.json +++ b/plugin/chrome/_locales/en/messages.json @@ -8,6 +8,7 @@ "corresponds_label": { "message": "This corresponds to:" }, "no_data": { "message": "No data" }, "more_details": { "message": "More details" }, + "connected": { "message": "You are connected !"}, "save_result": { "message": "Do you want to save this result?" }, "login": { "message": "Log in" }, "negligible": { "message": "negligible" }, diff --git a/plugin/chrome/background.js b/plugin/chrome/background.js index a3cbf05b..e9a8d08e 100644 --- a/plugin/chrome/background.js +++ b/plugin/chrome/background.js @@ -22,11 +22,12 @@ let countryCache = { const CARBON_CACHE_TTL = 3600000; // 1 heure const carbonIntensityCache = new Map(); +// Flag pour éviter de spammer la notification +let hasNotifiedExpiration = false; -// Information nécessaire pour appeler les APIs -const token = CONFIG.BACKEND.ELECTRICITY_MAP_API_KEY; +const token = CONFIG.BACKEND.ELECTRICITY_MAP_API_KEY; const carbonIntensityUrl = - "https://api.electricitymap.org/v3/carbon-intensity/latest"; + "https://api.electricitymap.org/v3/carbon-intensity/latest"; function getTabData(tabId) { if (!tabNetworkData.has(tabId)) { @@ -170,14 +171,17 @@ function extractDomain(url) { async function getUserId() { try { - const cookies = await chrome.cookies.getAll({ - domain: CONFIG.BACKEND.DOMAIN, + const sessionCookie = await chrome.cookies.get({ + url: `${CONFIG.BACKEND.BASE_URL}/`, + name: "greenscoreweb_sessions", }); - const sessionCookie = cookies.find((cookie) => cookie.name === "greenscoreweb_sessions"); - if (!sessionCookie) { console.log("Pas de cookie de session trouvé"); + if (userCache.data !== null && !hasNotifiedExpiration) { + showSessionExpiredNotification(); + hasNotifiedExpiration = true; + } userCache.data = null; return null; } @@ -202,17 +206,34 @@ async function getUserId() { } const userData = await response.json(); + // Compat backend: certains endpoints renvoient `user_full` au lieu de `account`. + const accountData = userData.account || userData.user_full || null; - userCache.data = userData.account; + userCache.data = accountData; userCache.timestamp = now; + hasNotifiedExpiration = false; // Réinitialise si connexion réussie - return userData.account; + return accountData; } catch (error) { console.error("Erreur lors de la récupération de l'ID:", error); + if (userCache.data !== null && !hasNotifiedExpiration) { + showSessionExpiredNotification(); + hasNotifiedExpiration = true; + } + userCache.data = null; return null; } } +function showSessionExpiredNotification() { + chrome.notifications.create({ + type: "basic", + title: "Session Expirée", + message: "Votre session GreenScore a expiré. Veuillez vous reconnecter.", + iconUrl: chrome.runtime.getURL("assets/images/logo.png") + }); +} + async function getUserData() { try { const userData = await getUserId(); @@ -692,7 +713,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return { success: true, equivalents: equivalents.map((eq) => ({ - image: "../assets/images/equivalents/" + eq.icon, + image: "assets/images/equivalents/" + eq.icon, value: parseFloat(eq.value).toFixed(1), name: eq.name, })), @@ -721,7 +742,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { const activeTab = tabs[0]; // Vérification localhost - if (isLocalDomain(activeTab.url)) { + // `checkLoginStatus` doit rester disponible même sur GreenScore pour déclencher get-account. + if (message.type !== "checkLoginStatus" && isLocalDomain(activeTab.url)) { await chrome.runtime.sendMessage({ type: "localhostDetected", message: "Vous êtes bien arrivé sur notre site ;)", diff --git a/plugin/chrome/manifest.json b/plugin/chrome/manifest.json index bd833cb2..52bb7d0c 100644 --- a/plugin/chrome/manifest.json +++ b/plugin/chrome/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "GreenScore", - "version": "1.5.0", + "version": "1.6.0", "description": "GreenScore mesure l’empreinte carbone des sites web par utilisateur et organisation.", "default_locale": "en", @@ -16,9 +16,11 @@ "permissions": [ "tabs", + "storage", "cookies", "webNavigation", - "webRequest" + "webRequest", + "notifications" ], "host_permissions": [ diff --git a/plugin/chrome/popup.html b/plugin/chrome/popup.html index a5a3fd81..53f7282d 100644 --- a/plugin/chrome/popup.html +++ b/plugin/chrome/popup.html @@ -109,6 +109,7 @@

Plus de détails diff --git a/plugin/chrome/popup.js b/plugin/chrome/popup.js index 9c05d09e..2c2dfa34 100644 --- a/plugin/chrome/popup.js +++ b/plugin/chrome/popup.js @@ -274,10 +274,10 @@ document.addEventListener("DOMContentLoaded", async () => { const params = new URLSearchParams({ country: response.country || "", url_full: response.urlFull || "", - totalConsu: gCO2eValue || 0, - pageSize: response.totalResourceSize || 0, - loadingTime: response.loadTime || 0, - queriesQuantity: response.totalRequests || 0, + carbon_footprint: gCO2eValue || 0, + data_transferred: response.totalResourceSize || 0, + loading_time: response.loadTime || 0, + queries_quantity: response.totalRequests || 0, }); url += "?" + params.toString(); } diff --git a/plugin/firefox/manifest.json b/plugin/firefox/manifest.json index 1345cd1e..f3328756 100644 --- a/plugin/firefox/manifest.json +++ b/plugin/firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "GreenScore", - "version": "1.3", + "version": "1.6.0", "description": "GreenScore est un plugin et une plateforme web qui mesure l'empreinte carbone d'un site par utilisateur et organisation. Il offre des tableaux de bord personnels et d'organisation, des analyses ponctuelles, et un classement des sites les plus énergivores pour sensibiliser et encourager des pratiques numériques durables.", "browser_specific_settings": { "gecko": { @@ -22,6 +22,7 @@ "webNavigation", "tabs", "scripting", + "notifications", "http://127.0.0.1/index.php" ], "background": { diff --git a/plugin/firefox/src/background.js b/plugin/firefox/src/background.js index 915289f7..557edfff 100644 --- a/plugin/firefox/src/background.js +++ b/plugin/firefox/src/background.js @@ -20,6 +20,8 @@ let countryCache = { const CARBON_CACHE_TTL = 3600000; // 1 heure const carbonIntensityCache = new Map(); +// Flag pour éviter de spammer la notification +let hasNotifiedExpiration = false; // Information nécessaire pour appeler les APIs const token = CONFIG.BACKEND.ELECTRICITY_MAP_API_KEY; @@ -169,14 +171,17 @@ function extractDomain(url) { async function getUserId() { try { - const cookies = await browser.cookies.getAll({ - domain: CONFIG.BACKEND.DOMAIN, + const sessionCookie = await browser.cookies.get({ + url: `${CONFIG.BACKEND.BASE_URL}/`, + name: "greenscoreweb_sessions", }); - const sessionCookie = cookies.find((cookie) => cookie.name === "greenscoreweb_sessions"); - if (!sessionCookie) { console.log("Pas de cookie de session trouvé"); + if (userCache.data !== null && !hasNotifiedExpiration) { + showSessionExpiredNotification(); + hasNotifiedExpiration = true; + } userCache.data = null; return null; } @@ -201,17 +206,33 @@ async function getUserId() { } const userData = await response.json(); + const accountData = userData.account || userData.user_full || null; - userCache.data = userData.account; + userCache.data = accountData; userCache.timestamp = now; + hasNotifiedExpiration = false; // Réinitialise si connexion réussie - return userData.account; + return accountData; } catch (error) { console.error("Erreur lors de la récupération de l'ID:", error); + if (userCache.data !== null && !hasNotifiedExpiration) { + showSessionExpiredNotification(); + hasNotifiedExpiration = true; + } + userCache.data = null; return null; } } +function showSessionExpiredNotification() { + browser.notifications.create({ + type: "basic", + title: "Session Expirée", + message: "Votre session GreenScore a expiré. Veuillez vous reconnecter.", + iconUrl: browser.runtime.getURL("assets/images/logo.png") + }); +} + async function getUserData() { try { const userData = await getUserId(); @@ -722,7 +743,7 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => { const activeTab = tabs[0]; // Vérification localhost - if (isLocalDomain(activeTab.url)) { + if (message.type !== "checkLoginStatus" && isLocalDomain(activeTab.url)) { await browser.runtime.sendMessage({ type: "localhostDetected", message: "Vous êtes bien arrivé sur notre site ;)", diff --git a/plugin/firefox/src/popup.js b/plugin/firefox/src/popup.js index d7cc450e..e834ac0b 100644 --- a/plugin/firefox/src/popup.js +++ b/plugin/firefox/src/popup.js @@ -323,10 +323,10 @@ document.addEventListener("DOMContentLoaded", async () => { const params = new URLSearchParams({ country: response.country || "", url_full: response.urlFull || "", - totalConsu: gCO2eValue || 0, - pageSize: response.totalResourceSize || 0, - loadingTime: response.loadTime || 0, - queriesQuantity: response.totalRequests || 0, + carbon_footprint: gCO2eValue || 0, + data_transferred: response.totalResourceSize || 0, + loading_time: response.loadTime || 0, + queries_quantity: response.totalRequests || 0, }); url += "?" + params.toString(); } @@ -342,14 +342,11 @@ document.addEventListener("DOMContentLoaded", async () => { const urlElement = document.getElementById("site-url"); const countryElement = document.getElementById("site-country"); - if (countryElement && urlElement) { - countryElement.textContent = t("country_consumption_intro", { countryName: response.country }); - urlElement.textContent = response.url; - } - } - }) - .catch(() => { - }); + if (countryElement && urlElement) { + countryElement.textContent = t("country_consumption_intro", { countryName: countryResponse.country }); + urlElement.textContent = countryResponse.url; + } + } // 4. Equivalents try {