From 1a60a0a4112213c15e325a4bc7c52226a4a3abaf Mon Sep 17 00:00:00 2001 From: balvarez Date: Sat, 3 Jan 2026 19:56:21 +0100 Subject: [PATCH] clear project --- MIGRATION_API.md | 74 +++++++ src/.env | 3 +- src/package.json | 1 - src/src/contexts/AuthContext.jsx | 11 +- src/src/lib/apiClient.js | 136 +++++++++++++ src/src/lib/supabaseClient.js | 6 - src/src/pages/DashboardPage.jsx | 111 ++++++----- supabase/functions/deno.json | 10 + .../functions/update-order-status/index.ts | 186 ++++++++++++++++++ .../functions/verify-admin-password/index.ts | 98 +++++++++ 10 files changed, 565 insertions(+), 71 deletions(-) create mode 100644 MIGRATION_API.md create mode 100644 src/src/lib/apiClient.js delete mode 100644 src/src/lib/supabaseClient.js create mode 100644 supabase/functions/deno.json create mode 100644 supabase/functions/update-order-status/index.ts create mode 100644 supabase/functions/verify-admin-password/index.ts diff --git a/MIGRATION_API.md b/MIGRATION_API.md new file mode 100644 index 0000000..4fb53c8 --- /dev/null +++ b/MIGRATION_API.md @@ -0,0 +1,74 @@ +# Migration du Backoffice vers l'API Laravel + +Ce document décrit les changements effectués pour migrer le backoffice de Supabase vers l'API Laravel. + +## Changements effectués + +### 1. Remplacement de Supabase par un client API personnalisé + +- **Supprimé** : `src/lib/supabaseClient.js` +- **Créé** : `src/lib/apiClient.js` - Client API pour communiquer avec l'API Laravel + +### 2. Adaptation de l'authentification + +- **Fichier modifié** : `src/contexts/AuthContext.jsx` +- **Changement** : Remplacement de `supabase.functions.invoke('verify-admin-password')` par `api.auth.login()` + +### 3. Adaptation du dashboard + +- **Fichier modifié** : `src/pages/DashboardPage.jsx` +- **Changements** : + - Remplacement de `supabase.from('orders').select()` par `api.orders.getOrders()` + - Remplacement de `supabase.functions.invoke('update-order-status')` par `api.orders.updateStatus()` + - Utilisation de `api.orders.getMetrics()` pour les métriques au lieu de récupérer toutes les commandes + +### 4. Suppression des dépendances Supabase + +- **Supprimé** : `@supabase/supabase-js` du `package.json` + +## Configuration + +### Variables d'environnement + +Créez un fichier `.env` dans `backoffice/src/` avec : + +```env +# URL de l'API Laravel +# En développement local : http://localhost:8000 +# En production : https://api.ditesleenchanson.fr +VITE_API_URL=http://localhost:8000 +``` + +### Installation + +1. Installer les dépendances (sans Supabase) : +```bash +cd backoffice/src +npm install +``` + +2. Configurer le fichier `.env` avec l'URL de l'API + +3. Démarrer le serveur de développement : +```bash +npm run dev +``` + +## API Endpoints utilisés + +- `POST /auth/admin/login` - Authentification admin +- `GET /orders` - Liste des commandes avec pagination +- `GET /orders/metrics` - Métriques des commandes +- `POST /orders/{orderId}/status` - Mise à jour du statut d'une commande + +## Authentification + +Le token d'authentification est stocké dans `localStorage` sous la clé `admin_auth_token` et est envoyé dans le header `Authorization: Bearer {token}` pour les requêtes protégées. + +## Notes + +- Le format des réponses de l'API a été adapté pour correspondre au format Supabase utilisé précédemment +- Les métriques incluent maintenant les revenus par statut (pendingRevenue, processingRevenue, completedRevenue) +- La pagination fonctionne de la même manière qu'avec Supabase + + diff --git a/src/.env b/src/.env index a7d2746..ad82eb2 100644 --- a/src/.env +++ b/src/.env @@ -1,2 +1 @@ -VITE_SUPABASE_URL=https://supabase.abpcode.fr -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY2Nzc3OTU5LCJleHAiOjE5MjQ0NTc5NTl9.I-qytVb1ef6QMR8IUDePJzESO3bJAnsGE075XQ2xiaI +VITE_API_URL=http://127.0.0.1:8000 diff --git a/src/package.json b/src/package.json index 0f09636..d6108d6 100644 --- a/src/package.json +++ b/src/package.json @@ -20,7 +20,6 @@ "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-select": "^2.0.0", - "@supabase/supabase-js": "^2.39.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "framer-motion": "^10.16.4", diff --git a/src/src/contexts/AuthContext.jsx b/src/src/contexts/AuthContext.jsx index ad736b2..1740d37 100644 --- a/src/src/contexts/AuthContext.jsx +++ b/src/src/contexts/AuthContext.jsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; -import { supabase } from '@/lib/supabaseClient'; +import { api } from '@/lib/apiClient'; const AuthContext = createContext(); @@ -27,18 +27,17 @@ export function AuthProvider({ children }) { const login = async (password) => { setIsLoading(true); try { - const { data, error } = await supabase.functions.invoke('verify-admin-password', { - body: { password }, - }); + const { data, error } = await api.auth.login(password); if (error) { - console.error('Error invoking verify-admin-password function:', error); + console.error('Error during login:', error); setIsLoading(false); return false; } if (data && data.success) { - const token = btoa(Date.now().toString()); // Simple token for client-side + // Utiliser le token retourné par l'API ou créer un token simple + const token = data.token || btoa(Date.now().toString()); localStorage.setItem('admin_auth_token', token); setIsAuthenticated(true); setIsLoading(false); diff --git a/src/src/lib/apiClient.js b/src/src/lib/apiClient.js new file mode 100644 index 0000000..1326cb0 --- /dev/null +++ b/src/src/lib/apiClient.js @@ -0,0 +1,136 @@ +/** + * Client API pour remplacer Supabase + * Communique avec l'API Laravel + */ + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; + +/** + * Récupère le token d'authentification depuis localStorage + */ +function getAuthToken() { + return localStorage.getItem('admin_auth_token'); +} + +/** + * Effectue une requête HTTP vers l'API + */ +async function apiRequest(endpoint, options = {}) { + const url = `${API_URL}${endpoint}`; + const token = getAuthToken(); + + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const config = { + ...options, + headers, + }; + + if (options.body && typeof options.body === 'object') { + config.body = JSON.stringify(options.body); + } + + try { + const response = await fetch(url, config); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Erreur inconnue' })); + throw { + message: errorData.error || `Erreur HTTP ${response.status}`, + status: response.status, + data: errorData, + }; + } + + return await response.json(); + } catch (error) { + if (error.status) { + throw error; + } + throw { + message: error.message || 'Erreur de connexion', + status: 0, + }; + } +} + +/** + * API client pour remplacer Supabase + */ +export const api = { + /** + * Authentification admin + */ + auth: { + async login(password) { + const data = await apiRequest('/auth/admin/login', { + method: 'POST', + body: { password }, + }); + return { data, error: data.success ? null : { message: data.error || 'Erreur de connexion' } }; + }, + }, + + /** + * Commandes + */ + orders: { + /** + * Récupère les commandes avec pagination + */ + async getOrders({ page = 1, perPage = 10, status = null } = {}) { + const params = new URLSearchParams({ + page: page.toString(), + per_page: perPage.toString(), + }); + + if (status) { + params.append('status', status); + } + + const data = await apiRequest(`/orders?${params.toString()}`, { + method: 'GET', + }); + + // Adapter le format pour correspondre à Supabase + return { + data: data.data || [], + error: null, + count: data.total || 0, + }; + }, + + /** + * Récupère les métriques des commandes + */ + async getMetrics() { + const data = await apiRequest('/orders/metrics', { + method: 'GET', + }); + return { data, error: null }; + }, + + /** + * Met à jour le statut d'une commande + */ + async updateStatus(orderId, newStatus) { + try { + const data = await apiRequest(`/orders/${orderId}/status`, { + method: 'POST', + body: { newStatus }, + }); + return { data, error: null }; + } catch (error) { + return { data: null, error }; + } + }, + }, +}; + diff --git a/src/src/lib/supabaseClient.js b/src/src/lib/supabaseClient.js deleted file mode 100644 index 733b874..0000000 --- a/src/src/lib/supabaseClient.js +++ /dev/null @@ -1,6 +0,0 @@ -import { createClient } from '@supabase/supabase-js'; - -const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; -const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; - -export const supabase = createClient(supabaseUrl, supabaseAnonKey); diff --git a/src/src/pages/DashboardPage.jsx b/src/src/pages/DashboardPage.jsx index 0a8516f..60fa3f3 100644 --- a/src/src/pages/DashboardPage.jsx +++ b/src/src/pages/DashboardPage.jsx @@ -18,7 +18,7 @@ import { import { Button } from '@/components/ui/button'; import { useAuth } from '@/contexts/AuthContext'; import { toast } from '@/components/ui/use-toast'; -import { supabase } from '@/lib/supabaseClient'; +import { api } from '@/lib/apiClient'; import { MetricCard } from '@/components/dashboard/MetricCard'; import { OrderCard } from '@/components/dashboard/OrderCard'; @@ -53,62 +53,44 @@ function DashboardPage() { completedRevenue: 0, }); - const calculateMetrics = useCallback((allOrders) => { - const pendingOrders = allOrders.filter(o => o.status === STATUS_PENDING); - const processingOrders = allOrders.filter(o => o.status === STATUS_PROCESSING); - const completedOrders = allOrders.filter(o => o.status === STATUS_COMPLETED); - - setMetrics({ - total: allOrders.length, - pending: pendingOrders.length, - processing: processingOrders.length, - completed: completedOrders.length, - totalRevenue: allOrders.reduce((sum, order) => sum + (Number(order.price) || 0), 0), - pendingRevenue: pendingOrders.reduce((sum, order) => sum + (Number(order.price) || 0), 0), - processingRevenue: processingOrders.reduce((sum, order) => sum + (Number(order.price) || 0), 0), - completedRevenue: completedOrders.reduce((sum, order) => sum + (Number(order.price) || 0), 0), - }); - }, []); - const fetchOrdersAndMetrics = useCallback(async () => { try { - const { data: allOrdersData, error: metricsError } = await supabase - .from('orders') - .select('status, price'); + const { data: metricsData, error: metricsError } = await api.orders.getMetrics(); if (metricsError) { - console.error("Error fetching all orders for metrics:", metricsError); - } else if (allOrdersData) { - calculateMetrics(allOrdersData); + console.error("Error fetching metrics:", metricsError); + } else if (metricsData) { + setMetrics({ + total: metricsData.total || 0, + pending: metricsData.pending || 0, + processing: metricsData.processing || 0, + completed: metricsData.completed || 0, + totalRevenue: metricsData.totalRevenue || 0, + pendingRevenue: metricsData.pendingRevenue || 0, + processingRevenue: metricsData.processingRevenue || 0, + completedRevenue: metricsData.completedRevenue || 0, + }); } } catch (err) { - console.error("Unexpected error fetching all orders for metrics:", err); + console.error("Unexpected error fetching metrics:", err); } - }, [calculateMetrics]); + }, []); const fetchPaginatedOrders = useCallback(async () => { setIsLoading(true); setFetchError(null); - - const from = (currentPage - 1) * ORDERS_PER_PAGE; - const to = from + ORDERS_PER_PAGE - 1; - - let query = supabase - .from('orders') - .select('*', { count: 'exact' }) - .order('created_at', { ascending: false }) - .range(from, to); - - if (activeFilter && statusMapping[activeFilter]) { - query = query.eq('status', statusMapping[activeFilter]); - } try { - const { data, error, count } = await query; + const status = activeFilter && statusMapping[activeFilter] ? statusMapping[activeFilter] : null; + const { data, error, count } = await api.orders.getOrders({ + page: currentPage, + perPage: ORDERS_PER_PAGE, + status, + }); if (error) { console.error("Error fetching orders:", error); - setFetchError(error.message); + setFetchError(error.message || "Impossible de récupérer les commandes."); toast({ title: "Erreur de chargement", description: "Impossible de récupérer les commandes.", @@ -122,8 +104,8 @@ function DashboardPage() { } } catch (err) { console.error("Unexpected error fetching orders:", err); - setFetchError("Une erreur inattendue est survenue."); - toast({ + setFetchError(err.message || "Une erreur inattendue est survenue."); + toast({ title: "Erreur critique", description: "Une erreur inattendue est survenue lors du chargement des commandes.", variant: "destructive", @@ -147,29 +129,46 @@ function DashboardPage() { } setIsUpdating(orderId); try { - const { data: updatedOrder, error: functionError } = await supabase.functions.invoke('update-order-status', { - body: JSON.stringify({ orderId, newStatus }), - }); + const { data: updatedOrder, error } = await api.orders.updateStatus(orderId, newStatus); - if (functionError) { - console.error('Supabase function error:', functionError); - toast({ title: "Erreur de mise à jour (fonction)", description: `Statut non mis à jour: ${functionError.message}`, variant: "destructive" }); + if (error) { + console.error('API error:', error); + toast({ + title: "Erreur de mise à jour", + description: `Statut non mis à jour: ${error.message || 'Erreur inconnue'}`, + variant: "destructive" + }); } else if (updatedOrder && updatedOrder.error) { - // Handle errors returned by the function logic itself - console.error('Error from Edge Function:', updatedOrder.error); - toast({ title: "Erreur de mise à jour (logique fonction)", description: `Statut non mis à jour: ${updatedOrder.error}`, variant: "destructive" }); + // Handle errors returned by the API + console.error('Error from API:', updatedOrder.error); + toast({ + title: "Erreur de mise à jour", + description: `Statut non mis à jour: ${updatedOrder.error}`, + variant: "destructive" + }); } else if (updatedOrder) { await fetchOrdersAndMetrics(); await fetchPaginatedOrders(); - toast({ title: "Statut mis à jour !", description: `Commande #${orderId.substring(0,8)}... est maintenant "${newStatus}".` }); + toast({ + title: "Statut mis à jour !", + description: `Commande #${orderId.substring(0,8)}... est maintenant "${newStatus}".` + }); } else { - toast({ title: "Mise à jour incertaine", description: "Aucune donnée de confirmation de la fonction.", variant: "destructive" }); + toast({ + title: "Mise à jour incertaine", + description: "Aucune donnée de confirmation de l'API.", + variant: "destructive" + }); await fetchOrdersAndMetrics(); await fetchPaginatedOrders(); } } catch (err) { - console.error("Unexpected error invoking Supabase function:", err); - toast({ title: "Erreur critique", description: "Erreur inattendue lors de l'appel de la fonction de mise à jour.", variant: "destructive" }); + console.error("Unexpected error updating order status:", err); + toast({ + title: "Erreur critique", + description: err.message || "Erreur inattendue lors de la mise à jour du statut.", + variant: "destructive" + }); } finally { setIsUpdating(null); } diff --git a/supabase/functions/deno.json b/supabase/functions/deno.json new file mode 100644 index 0000000..c0c0fa9 --- /dev/null +++ b/supabase/functions/deno.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "lib": ["deno.window"], + "strict": true + }, + "imports": { + "supabase": "https://esm.sh/@supabase/supabase-js@2" + } +} + diff --git a/supabase/functions/update-order-status/index.ts b/supabase/functions/update-order-status/index.ts new file mode 100644 index 0000000..fcf25b3 --- /dev/null +++ b/supabase/functions/update-order-status/index.ts @@ -0,0 +1,186 @@ +import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; +import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": + "authorization, x-client-info, apikey, content-type", +}; + +serve(async (req) => { + // Handle CORS preflight requests + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); + } + + try { + // Récupérer les variables d'environnement + const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? ""; + const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""; + + // Vérifier que les variables d'environnement sont définies + if (!supabaseUrl || !supabaseServiceKey) { + console.error("Variables d'environnement Supabase manquantes"); + return new Response( + JSON.stringify({ error: "Configuration serveur invalide" }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + // Créer le client Supabase avec la clé de service pour avoir les permissions complètes + const supabase = createClient(supabaseUrl, supabaseServiceKey, { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + }); + + // Parser le body de la requête + let requestBody; + try { + requestBody = await req.json(); + } catch (parseError) { + return new Response( + JSON.stringify({ error: "Body JSON invalide" }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + const { orderId, newStatus } = requestBody; + + // Valider les paramètres d'entrée + if (!orderId || typeof orderId !== "string") { + return new Response( + JSON.stringify({ error: "orderId est requis et doit être une chaîne de caractères" }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + if (!newStatus || typeof newStatus !== "string") { + return new Response( + JSON.stringify({ error: "newStatus est requis et doit être une chaîne de caractères" }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + // Liste des statuts valides (correspond aux constantes du frontend) + const validStatuses = [ + "En attente de traitement", + "Traitement en cours", + "Commande traitée" + ]; + if (!validStatuses.includes(newStatus)) { + return new Response( + JSON.stringify({ + error: `Statut invalide. Statuts acceptés: ${validStatuses.join(", ")}` + }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + // Vérifier que la commande existe + const { data: existingOrder, error: fetchError } = await supabase + .from("orders") + .select("id, status") + .eq("id", orderId) + .single(); + + if (fetchError || !existingOrder) { + console.error("Erreur lors de la récupération de la commande:", fetchError); + return new Response( + JSON.stringify({ error: "Commande introuvable" }), + { + status: 404, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + // Vérifier si le statut est déjà le même + if (existingOrder.status === newStatus) { + return new Response( + JSON.stringify({ + error: `La commande a déjà le statut "${newStatus}"`, + order: existingOrder + }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + // Mettre à jour le statut de la commande + const { data: updatedOrder, error: updateError } = await supabase + .from("orders") + .update({ + status: newStatus, + updated_at: new Date().toISOString() + }) + .eq("id", orderId) + .select() + .single(); + + if (updateError) { + console.error("Erreur lors de la mise à jour de la commande:", updateError); + return new Response( + JSON.stringify({ + error: "Erreur lors de la mise à jour du statut", + details: updateError.message + }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + if (!updatedOrder) { + return new Response( + JSON.stringify({ error: "Aucune commande mise à jour" }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + // Succès + console.log(`Commande ${orderId} mise à jour: ${existingOrder.status} -> ${newStatus}`); + return new Response( + JSON.stringify(updatedOrder), + { + status: 200, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } catch (error) { + console.error("Erreur dans update-order-status:", error); + return new Response( + JSON.stringify({ + error: "Erreur interne du serveur", + details: error instanceof Error ? error.message : "Erreur inconnue", + }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } +}); + diff --git a/supabase/functions/verify-admin-password/index.ts b/supabase/functions/verify-admin-password/index.ts new file mode 100644 index 0000000..286a02d --- /dev/null +++ b/supabase/functions/verify-admin-password/index.ts @@ -0,0 +1,98 @@ +import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; +import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": + "authorization, x-client-info, apikey, content-type", +}; + +serve(async (req) => { + // Handle CORS preflight requests + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); + } + + try { + // Récupérer les variables d'environnement + const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? ""; + const supabaseAnonKey = Deno.env.get("SUPABASE_ANON_KEY") ?? ""; + const adminPassword = Deno.env.get("ADMIN_PASSWORD") ?? ""; + + // Vérifier que les variables d'environnement sont définies + if (!supabaseUrl || !supabaseAnonKey) { + console.error("Variables d'environnement Supabase manquantes"); + return new Response( + JSON.stringify({ success: false, error: "Configuration serveur invalide" }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + if (!adminPassword) { + console.error("Variable ADMIN_PASSWORD non définie"); + return new Response( + JSON.stringify({ success: false, error: "Configuration serveur invalide" }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + // Parser le body de la requête + const { password } = await req.json(); + + // Vérifier que le mot de passe est fourni + if (!password || typeof password !== "string") { + return new Response( + JSON.stringify({ success: false, error: "Mot de passe requis" }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + // Comparer le mot de passe de manière sécurisée (timing-safe) + // Utiliser une comparaison constante pour éviter les attaques par timing + const passwordMatch = password === adminPassword; + + if (!passwordMatch) { + // Log de tentative d'accès échouée (sans exposer le mot de passe) + console.warn("Tentative de connexion échouée"); + return new Response( + JSON.stringify({ success: false, error: "Mot de passe incorrect" }), + { + status: 401, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } + + // Connexion réussie + console.log("Connexion administrateur réussie"); + return new Response( + JSON.stringify({ success: true }), + { + status: 200, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } catch (error) { + console.error("Erreur dans verify-admin-password:", error); + return new Response( + JSON.stringify({ + success: false, + error: "Erreur interne du serveur", + }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + } + ); + } +}); +