clear project
This commit is contained in:
74
MIGRATION_API.md
Normal file
74
MIGRATION_API.md
Normal file
@@ -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
|
||||||
|
|
||||||
|
|
||||||
3
src/.env
3
src/.env
@@ -1,2 +1 @@
|
|||||||
VITE_SUPABASE_URL=https://supabase.abpcode.fr
|
VITE_API_URL=http://127.0.0.1:8000
|
||||||
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY2Nzc3OTU5LCJleHAiOjE5MjQ0NTc5NTl9.I-qytVb1ef6QMR8IUDePJzESO3bJAnsGE075XQ2xiaI
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-slider": "^1.1.2",
|
"@radix-ui/react-slider": "^1.1.2",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@supabase/supabase-js": "^2.39.0",
|
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
import { supabase } from '@/lib/supabaseClient';
|
import { api } from '@/lib/apiClient';
|
||||||
|
|
||||||
const AuthContext = createContext();
|
const AuthContext = createContext();
|
||||||
|
|
||||||
@@ -27,18 +27,17 @@ export function AuthProvider({ children }) {
|
|||||||
const login = async (password) => {
|
const login = async (password) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase.functions.invoke('verify-admin-password', {
|
const { data, error } = await api.auth.login(password);
|
||||||
body: { password },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error invoking verify-admin-password function:', error);
|
console.error('Error during login:', error);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data && data.success) {
|
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);
|
localStorage.setItem('admin_auth_token', token);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|||||||
136
src/src/lib/apiClient.js
Normal file
136
src/src/lib/apiClient.js
Normal file
@@ -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 };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
@@ -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);
|
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { toast } from '@/components/ui/use-toast';
|
import { toast } from '@/components/ui/use-toast';
|
||||||
import { supabase } from '@/lib/supabaseClient';
|
import { api } from '@/lib/apiClient';
|
||||||
import { MetricCard } from '@/components/dashboard/MetricCard';
|
import { MetricCard } from '@/components/dashboard/MetricCard';
|
||||||
import { OrderCard } from '@/components/dashboard/OrderCard';
|
import { OrderCard } from '@/components/dashboard/OrderCard';
|
||||||
|
|
||||||
@@ -53,62 +53,44 @@ function DashboardPage() {
|
|||||||
completedRevenue: 0,
|
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 () => {
|
const fetchOrdersAndMetrics = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const { data: allOrdersData, error: metricsError } = await supabase
|
const { data: metricsData, error: metricsError } = await api.orders.getMetrics();
|
||||||
.from('orders')
|
|
||||||
.select('status, price');
|
|
||||||
|
|
||||||
if (metricsError) {
|
if (metricsError) {
|
||||||
console.error("Error fetching all orders for metrics:", metricsError);
|
console.error("Error fetching metrics:", metricsError);
|
||||||
} else if (allOrdersData) {
|
} else if (metricsData) {
|
||||||
calculateMetrics(allOrdersData);
|
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) {
|
} catch (err) {
|
||||||
console.error("Unexpected error fetching all orders for metrics:", err);
|
console.error("Unexpected error fetching metrics:", err);
|
||||||
}
|
}
|
||||||
}, [calculateMetrics]);
|
}, []);
|
||||||
|
|
||||||
const fetchPaginatedOrders = useCallback(async () => {
|
const fetchPaginatedOrders = useCallback(async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setFetchError(null);
|
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 {
|
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) {
|
if (error) {
|
||||||
console.error("Error fetching orders:", error);
|
console.error("Error fetching orders:", error);
|
||||||
setFetchError(error.message);
|
setFetchError(error.message || "Impossible de récupérer les commandes.");
|
||||||
toast({
|
toast({
|
||||||
title: "Erreur de chargement",
|
title: "Erreur de chargement",
|
||||||
description: "Impossible de récupérer les commandes.",
|
description: "Impossible de récupérer les commandes.",
|
||||||
@@ -122,8 +104,8 @@ function DashboardPage() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Unexpected error fetching orders:", err);
|
console.error("Unexpected error fetching orders:", err);
|
||||||
setFetchError("Une erreur inattendue est survenue.");
|
setFetchError(err.message || "Une erreur inattendue est survenue.");
|
||||||
toast({
|
toast({
|
||||||
title: "Erreur critique",
|
title: "Erreur critique",
|
||||||
description: "Une erreur inattendue est survenue lors du chargement des commandes.",
|
description: "Une erreur inattendue est survenue lors du chargement des commandes.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
@@ -147,29 +129,46 @@ function DashboardPage() {
|
|||||||
}
|
}
|
||||||
setIsUpdating(orderId);
|
setIsUpdating(orderId);
|
||||||
try {
|
try {
|
||||||
const { data: updatedOrder, error: functionError } = await supabase.functions.invoke('update-order-status', {
|
const { data: updatedOrder, error } = await api.orders.updateStatus(orderId, newStatus);
|
||||||
body: JSON.stringify({ orderId, newStatus }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (functionError) {
|
if (error) {
|
||||||
console.error('Supabase function error:', functionError);
|
console.error('API error:', error);
|
||||||
toast({ title: "Erreur de mise à jour (fonction)", description: `Statut non mis à jour: ${functionError.message}`, variant: "destructive" });
|
toast({
|
||||||
|
title: "Erreur de mise à jour",
|
||||||
|
description: `Statut non mis à jour: ${error.message || 'Erreur inconnue'}`,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
} else if (updatedOrder && updatedOrder.error) {
|
} else if (updatedOrder && updatedOrder.error) {
|
||||||
// Handle errors returned by the function logic itself
|
// Handle errors returned by the API
|
||||||
console.error('Error from Edge Function:', updatedOrder.error);
|
console.error('Error from API:', updatedOrder.error);
|
||||||
toast({ title: "Erreur de mise à jour (logique fonction)", description: `Statut non mis à jour: ${updatedOrder.error}`, variant: "destructive" });
|
toast({
|
||||||
|
title: "Erreur de mise à jour",
|
||||||
|
description: `Statut non mis à jour: ${updatedOrder.error}`,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
} else if (updatedOrder) {
|
} else if (updatedOrder) {
|
||||||
await fetchOrdersAndMetrics();
|
await fetchOrdersAndMetrics();
|
||||||
await fetchPaginatedOrders();
|
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 {
|
} 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 fetchOrdersAndMetrics();
|
||||||
await fetchPaginatedOrders();
|
await fetchPaginatedOrders();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Unexpected error invoking Supabase function:", err);
|
console.error("Unexpected error updating order status:", err);
|
||||||
toast({ title: "Erreur critique", description: "Erreur inattendue lors de l'appel de la fonction de mise à jour.", variant: "destructive" });
|
toast({
|
||||||
|
title: "Erreur critique",
|
||||||
|
description: err.message || "Erreur inattendue lors de la mise à jour du statut.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsUpdating(null);
|
setIsUpdating(null);
|
||||||
}
|
}
|
||||||
|
|||||||
10
supabase/functions/deno.json
Normal file
10
supabase/functions/deno.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["deno.window"],
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"supabase": "https://esm.sh/@supabase/supabase-js@2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
186
supabase/functions/update-order-status/index.ts
Normal file
186
supabase/functions/update-order-status/index.ts
Normal file
@@ -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" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
98
supabase/functions/verify-admin-password/index.ts
Normal file
98
supabase/functions/verify-admin-password/index.ts
Normal file
@@ -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" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user