Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions backend/src/controllers/lpc_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MySqlPool>,
AuthenticatedUser(user_full): AuthenticatedUser,
Query(params): Query<LastPageConsultedInfos>,
session: Session,
Query(params): Query<LastPageConsultedQuery>,
) -> Result<Json<LastPageConsultedResponse>, AppError> {

let user_id = Some(user_full.user.id);
let user_id: Option<i64> = session.get("user_full").await
.ok()
.and_then(|user_full: Option<UserFull>| user_full.map(|u| u.user.id));
let infos: Option<LastPageConsultedInfos> = 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))
Expand Down
24 changes: 24 additions & 0 deletions backend/src/dto/lpc_dto.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -10,6 +11,29 @@ pub struct LastPageConsultedInfos {
pub country: String,
}

#[derive(Debug, Deserialize, Clone, Default, PartialEq)]
pub struct LastPageConsultedQuery {
pub url_full: Option<String>,
pub queries_quantity: Option<i32>,
pub carbon_footprint: Option<f64>,
pub data_transferred: Option<f64>,
pub loading_time: Option<f64>,
pub country: Option<String>,
}

impl LastPageConsultedQuery {
pub fn into_infos(self) -> Option<LastPageConsultedInfos> {
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,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
38 changes: 33 additions & 5 deletions backend/tests/lpc_controller_tests.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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(),
Expand All @@ -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
Expand All @@ -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");
}
44 changes: 26 additions & 18 deletions frontend/src/routes/(app)/derniere-page-consultee/+page.server.ts
Original file line number Diff line number Diff line change
@@ -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()) {
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
}
1 change: 1 addition & 0 deletions plugin/chrome/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
44 changes: 33 additions & 11 deletions plugin/chrome/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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();
Expand Down Expand Up @@ -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,
})),
Expand Down Expand Up @@ -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 ;)",
Expand Down
6 changes: 4 additions & 2 deletions plugin/chrome/manifest.json
Original file line number Diff line number Diff line change
@@ -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",

Expand All @@ -16,9 +16,11 @@

"permissions": [
"tabs",
"storage",
"cookies",
"webNavigation",
"webRequest"
"webRequest",
"notifications"
],

"host_permissions": [
Expand Down
1 change: 1 addition & 0 deletions plugin/chrome/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ <h1 class="text-2xl font-outfit font-extrabold text-gs-green-950" data-i18n="ext
id="details-button"
href="#"
class="flex justify-center items-center py-2 text-white font-outfit font-medium bg-gs-green-950 rounded-lg w-full h-fit"
data-i18n="more_details"
>
Plus de détails
</a>
Expand Down
8 changes: 4 additions & 4 deletions plugin/chrome/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
3 changes: 2 additions & 1 deletion plugin/firefox/manifest.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -22,6 +22,7 @@
"webNavigation",
"tabs",
"scripting",
"notifications",
"http://127.0.0.1/index.php"
],
"background": {
Expand Down
Loading
Loading