diff --git a/src/components/store/ProductCard.jsx b/src/components/store/ProductCard.jsx index d9bcd74..3aee561 100644 --- a/src/components/store/ProductCard.jsx +++ b/src/components/store/ProductCard.jsx @@ -66,16 +66,16 @@ export default function ProductCard({ product, selectedPeriod, categoryName }) { )} -
+

{product.title}

{priceData.isAvailable ? ( -
+
{priceData.displayPrice} € {priceData.suffix}
-
+
{!priceData.isOnce && ( <> {priceData.originalPrice ? ( @@ -96,9 +96,9 @@ export default function ProductCard({ product, selectedPeriod, categoryName }) { )}
-
+
-
    , li: ({node, ...props}) =>
  • {props.children}
  • , p: ({node, ...props}) =>

    , strong: ({node, ...props}) => }}> +

      , li: ({node, ...props}) =>
    • {props.children}
    • , p: ({node, ...props}) =>

      , strong: ({node, ...props}) => }}> {product.description || "Aucune description technique."}

diff --git a/src/pages/app/Checkout.jsx b/src/pages/app/Checkout.jsx index 861a432..10bac82 100644 --- a/src/pages/app/Checkout.jsx +++ b/src/pages/app/Checkout.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; -import { resetCart, addToCart, checkoutCart, getProductList, getClientProfile } from '../../services/api'; +import { resetCart, addToCart, checkoutCart, getProductList, getClient } from '../../services/billing_api'; export default function Checkout() { const { productId } = useParams(); @@ -18,7 +18,7 @@ export default function Checkout() { useEffect(() => { const loadData = async () => { try { - // 1. On charge d'abord le produit (Requête Publique) + // 1. On charge d'abord le produit const productData = await getProductList(); const foundProduct = productData.list?.find(p => p.id === parseInt(productId)); @@ -29,15 +29,14 @@ export default function Checkout() { } setProduct(foundProduct); - // 2. Ensuite, on tente de charger le profil (Requête Privée) + // 2. Ensuite, on tente de charger le profil try { - const profileData = await getClientProfile(); + const profileData = await getClient(); setUserProfile(profileData); } catch (profileErr) { // Si on tombe ici, c'est que FOSSBilling refuse l'accès au profil. console.error("Rejet API Profil :", profileErr); setError("Accès refusé. Vous devez être connecté à votre compte pour provisionner une instance."); - // Tu pourras décommenter la ligne suivante plus tard pour forcer la redirection : // navigate('/login'); setIsLoading(false); return; diff --git a/src/pages/app/Dashboard.jsx b/src/pages/app/Dashboard.jsx index 58b2d24..4358746 100644 --- a/src/pages/app/Dashboard.jsx +++ b/src/pages/app/Dashboard.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { getClientOrders } from '../../services/api'; +import { getOrdersList } from '../../services/billing_api'; import { AlertCircle, Loader, FileText, X } from 'lucide-react'; // Importation des composants @@ -23,7 +23,7 @@ export default function Dashboard() { const fetchInventory = async () => { setIsLoading(true); try { - const data = await getClientOrders(); + const data = await getOrdersList(); if (data.list) { // LE FILTRE CHIRURGICAL PAR PREFIXE diff --git a/src/pages/app/Services.jsx b/src/pages/app/Services.jsx index e11a169..c998546 100644 --- a/src/pages/app/Services.jsx +++ b/src/pages/app/Services.jsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { getClientOrders, getOrderDetails } from '../../services/api'; +import { getOrdersList, getOrderDetails } from '../../services/billing_api'; import { useVPC } from '../../services/useVPC'; import { Server, Globe, Folder, Trash2, Loader, Plus, AlertCircle } from 'lucide-react'; @@ -25,7 +25,7 @@ export default function Services() { const fetchServices = useCallback(async () => { try { - const data = await getClientOrders(); + const data = await getOrdersList(); if (data.list && data.list.length > 0) { const detailedServices = await Promise.all(data.list.map(async (order) => { let hDetails = null; diff --git a/src/pages/app/Store.jsx b/src/pages/app/Store.jsx index 117fac6..cb357a6 100644 --- a/src/pages/app/Store.jsx +++ b/src/pages/app/Store.jsx @@ -1,8 +1,8 @@ // src/pages/app/Store.jsx import { useState, useEffect } from 'react'; import { Server, Database, Cloud, Globe, Loader, AlertCircle } from 'lucide-react'; -import { getProductList } from '../../services/api'; -import CategorySection from '../../components/store/CategorySection'; // L'import magique +import { getProductList } from '../../services/billing_api'; +import CategorySection from '../../components/store/CategorySection'; export default function Store() { const [groupedProducts, setGroupedProducts] = useState({}); diff --git a/src/pages/public/Login.jsx b/src/pages/public/Login.jsx index c5a3864..4f7acf1 100644 --- a/src/pages/public/Login.jsx +++ b/src/pages/public/Login.jsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { useNavigate, Link } from 'react-router-dom'; -import { loginClient } from '../../services/api'; +import { loginClient } from '../../services/billing_api'; export default function Login() { const [email, setEmail] = useState(''); diff --git a/src/pages/public/Register.jsx b/src/pages/public/Register.jsx index 674188c..cd31aa8 100644 --- a/src/pages/public/Register.jsx +++ b/src/pages/public/Register.jsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { useNavigate, Link } from 'react-router-dom'; -import { registerUnifiedClient } from '../../services/api'; +import { createNewClient } from '../../services/billing_api'; import NotificationModal from '../../components/ui/NotificationModal'; export default function Register() { @@ -10,7 +10,7 @@ export default function Register() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); - + const [loading, setLoading] = useState(false); const [customAlert, setCustomAlert] = useState(null); const navigate = useNavigate(); @@ -28,15 +28,15 @@ export default function Register() { setCustomAlert({ type: 'error', title: 'Erreur de saisie', message: "Les clés d'accès ne correspondent pas." }); return; } - if (!/^[a-zA-Z0-9]{3,12}$/.test(username)) { + if (!/^[a-zA-Z0-9]{3,20}$/.test(username)) { setCustomAlert({ type: 'error', title: 'Identifiant invalide', message: "Le nom d'utilisateur doit contenir uniquement des lettres ou chiffres (entre 3 et 12 caractères)." }); return; } setLoading(true); try { - await registerUnifiedClient(email, username, password, firstName, lastName); - setCustomAlert({ type: 'success', title: 'PROVISIONNEMENT RÉUSSI', message: "Vos comptes FOSSBilling, HestiaCP et Nextcloud ont été initialisés.\n\nVous pouvez maintenant vous connecter." }); + await createNewClient(email, firstName, lastName, password, confirmPassword, "individual", username); + setCustomAlert({ type: 'success', title: 'PROVISIONNEMENT RÉUSSI', message: "Votre compte a été initialisé.\n\nVous pouvez maintenant vous connecter." }); } catch (err) { setCustomAlert({ type: 'error', title: 'Échec du Déploiement', message: err.message || "Échec de l'initialisation." }); } finally { @@ -56,7 +56,7 @@ export default function Register() { Créer un accès réseau

- [ INITIALISATION DU PROVISIONNEMENT TRIPLE ] + [ INITIALISATION DU PROVISIONNEMENT ]

diff --git a/src/services/api.js b/src/services/api.js index 07554f9..a323ac5 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -164,6 +164,42 @@ export const logoutClient = () => // GESTION DU CLIENT // ========================================== +// Creer un nouveau profil client +export const createClientProfile = (email, last_name, aid, gender, country, city, birthday, company, company_vat, company_number, type, address_1, address_2, postcode, state, phone, phone_cc, document_type, document_nr, notes, lang, custom_1, custom_2, custom_3, custom_4, custom_5, custom_6, custom_7, custom_8, custom_9, custom_10) => + apiCall(`${BASE_URL}/api/guest/profile/create`, { + email: email, + last_name: last_name, + aid: aid, + gender: gender, + country: country, + city: city, + birthday: birthday, + company: company, + company_vat: company_vat, + company_number: company_number, + type: type, + address_1: address_1, + address_2: address_2, + postcode: postcode, + state: state, + phone: phone, + phone_cc: phone_cc, + document_type: document_type, + document_nr: document_nr, + notes: notes, + lang: lang, + custom_1: custom_1, + custom_2: custom_2, + custom_3: custom_3, + custom_4: custom_4, + custom_5: custom_5, + custom_6: custom_6, + custom_7: custom_7, + custom_8: custom_8, + custom_9: custom_9, + custom_10: custom_10 + }); + // Recuperer les informations du client export const getClientProfile = () => apiCall(`${BASE_URL}/api/client/profile/get`, 'GET'); @@ -202,7 +238,6 @@ export const updateClientProfile = (email, last_name, aid, gender, country, city custom_8: custom_8, custom_9: custom_9, custom_10: custom_10 - }); // Changer le mot de passe diff --git a/src/services/billing_api.js b/src/services/billing_api.js new file mode 100644 index 0000000..8df04c7 --- /dev/null +++ b/src/services/billing_api.js @@ -0,0 +1,345 @@ +const BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; +const CUSTOM_API_BASE_URL = import.meta.env.VITE_CUSTOM_API_BASE_URL || ''; + +// Le moteur de requête +const apiCall = async (endpoint, param2 = 'GET', param3 = null) => { + let method = 'GET'; + let body = null; + + // DÉTECTION DE SIGNATURE (Le bouclier anti-crash) + if (typeof param2 === 'string') { + // Cas 1 : On a bien envoyé (URL, "POST", {données}) + method = param2.toUpperCase(); + body = param3; + } else if (typeof param2 === 'object' && param2 !== null) { + // Cas 2 : L'ancienne méthode a envoyé (URL, {données}) + // On redirige l'objet vers le body, et on force en POST + body = param2; + method = (typeof param3 === 'string') ? param3.toUpperCase() : 'POST'; + } + + // 1. Récupération du sésame + const token = localStorage.getItem('token'); + + // 2. Préparation de l'enveloppe + const headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + // 3. Configuration finale garantie sans objets égarés + const options = { + method: method, + headers: headers, + credentials: 'include' + }; + + if (body) { + options.body = JSON.stringify(body); + } + + try { + const response = await fetch(endpoint, options); + + if (response.status === 401 || response.status === 403) { + throw new Error("Accès refusé. Session expirée ou non valide."); + } + + const data = await response.json(); + + // Gestion des erreurs internes de l'API + if (data.error) { + throw new Error(data.error.message || "Erreur renvoyée par le serveur de facturation."); + } + + return data.result || data; + + } catch (error) { + console.error(`[API FAIL] ${method} ${endpoint} :`, error); + throw error; + } +}; + + +// Vide le panier (Action PUBLIQUE : on passe par l'API Guest) +export const resetCart = async () => { + try { + const cart = await apiCall(`${BASE_URL}/api/guest/cart/get`); + + if (cart && cart.items && cart.items.length > 0) { + for (const item of cart.items) { + await apiCall(`${BASE_URL}/api/guest/cart/remove_item`, { id: item.id }); + } + } + } catch (err) { + console.warn("Nettoyage du panier ignoré :", err); + } +}; + +// ========================================== +// ROUTES PERSONNALISÉES (CUSTOM API) +// ========================================== + +export const registerUnifiedClient = async (email, username, password, firstName, lastName) => { + try { + // Utilisation des backticks ici aussi ! + const response = await fetch(`${CUSTOM_API_BASE_URL}/custom_api/signup.php`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: email, + username: username, + password: password, + first_name: firstName, + last_name: lastName + }) + }); + + const data = await response.json(); + if (data.error) throw new Error(data.error.message); + return data.result; + } catch (err) { + console.error("Erreur lors de l'inscription unifiée :", err); + throw new Error("Échec de l'inscription. Veuillez réessayer plus tard."); + } +} + +// ####################################################################################### + +// ========================================== +// GESTION DES ACCES +// ========================================== + +// Connexion +export const loginClient = (email, password) => + apiCall(`${BASE_URL}/api/guest/client/login`, { email, password }); + +// Deconnexion +export const logoutClient = () => + apiCall(`${BASE_URL}/api/client/profile/logout`); + + +// ========================================== +// GESTION DU CLIENT +// ========================================== + +// Creer un nouveau profil client +export const createNewClient = ( + email, + first_name, + last_name, + password, + password_confirm, + type, + custom_1) => + apiCall(`${BASE_URL}/api/guest/client/create`, { + email: email, + first_name: first_name, + password: password, + password_confirm: password_confirm, + last_name: last_name, + type: type, + custom_1: custom_1 + }); + +// Recuperer les informations du client +export const getClient = () => + apiCall(`${BASE_URL}/api/client/profile/get`); + +// Update les informations du client +export const updateClient = (email, last_name, aid, gender, country, city, birthday, company, company_vat, company_number, type, address_1, address_2, postcode, state, phone, phone_cc, document_type, document_nr, notes, lang, custom_1, custom_2, custom_3, custom_4, custom_5, custom_6, custom_7, custom_8, custom_9, custom_10) => + apiCall(`${BASE_URL}/api/client/profile/update`, { + email: email, + last_name: last_name, + aid: aid, + gender: gender, + country: country, + city: city, + birthday: birthday, + company: company, + company_vat: company_vat, + company_number: company_number, + type: type, + address_1: address_1, + address_2: address_2, + postcode: postcode, + state: state, + phone: phone, + phone_cc: phone_cc, + document_type: document_type, + document_nr: document_nr, + notes: notes, + lang: lang, + custom_1: custom_1, + custom_2: custom_2, + custom_3: custom_3, + custom_4: custom_4, + custom_5: custom_5, + custom_6: custom_6, + custom_7: custom_7, + custom_8: custom_8, + custom_9: custom_9, + custom_10: custom_10 + + }); + +// Changer le mot de passe +export const changeClientPassword = (current_password, new_password) => + apiCall(`${BASE_URL}/api/client/profile/change_password`, { + current_password: current_password, + new_password: new_password, + confirm_password: new_password + }); + + + +// ========================================== +// GESTION DES ABONNEMENTS +// ========================================== + +// Souscription + +// Resiliation (après 14 jours) +export const cancelRenewal = (order_id) => + apiCall(`${BASE_URL}/api/admin/order/update`, { + id: order_id, + cancel_at_expiration: 1 + }); + +// Suppression +export const cancelOrder = (order_id) => + apiCall(`${BASE_URL}/api/client/order/delete`, { id: order_id }); + +// Promotion (Surclassement) +// Lister +export const getUpgrades = (order_id) => + apiCall(`${BASE_URL}/api/client/order/upgradables`, { id: order_id }); + +// Demotion (Declassement) + +// Lister les abonnements du client +export const getOrdersList = () => + apiCall(`${BASE_URL}/api/client/order/get_list`); + +// Lister les produits disponibles +export const getProductList = () => + apiCall(`${BASE_URL}/api/guest/product/get_list`); + +// Recuperer les details d'un abonnement +export const getOrderDetails = (order_id) => + apiCall(`${BASE_URL}/api/client/order/service`, { id: order_id }); + +// Recuperer les details secrets d'un abonnement +export const getServiceDetails = (orderId) => + apiCall(`${BASE_URL}/api/client/order/get`, { id: orderId }); + + +// ========================================== +// GESTION DU PANIER +// ========================================== + +// Ajouter au panier +export const addToCart = (productId, period, additionalData = {}) => + apiCall(`${BASE_URL}/api/guest/cart/add_item`, { + id: productId, + period: period, + ...additionalData // Les 3 petits points "étalent" le contenu de l'objet + }); + +// Checkout +export const checkoutCart = () => + apiCall(`${BASE_URL}/api/client/cart/checkout`, 'POST'); + + + +// ========================================== +// GESTION DES SERVICES D'HEBERGEMENT +// ========================================== + +// Modification du username +export const updateHostingUsername = (order_id, username) => + apiCall(`${BASE_URL}/api/client/servicehosting/change_username`, { + order_id: order_id, + username: username + }); + +// Modification du mot de passe +export const updateHostingPassword = (order_id, new_password) => + apiCall(`${BASE_URL}/api/client/servicehosting/change_password`, { + order_id: order_id, + password: new_password, + password_confirm: new_password + }); + +// Modification du domaine principal +export const updateHostingDomain = (order_id, new_domain) => + apiCall(`${BASE_URL}/api/client/servicehosting/change_domain`, { + order_id: order_id, + domain: new_domain + }); + +// Login URL (SSO) +export const getHostingLoginUrl = (order_id) => + apiCall(`${BASE_URL}/api/client/servicehosting/get_login_url`, { + order_id: order_id + }); + +// Modification du plan d'hébergement +export const updateHostingPlan = (order_id, new_plan_id) => + apiCall(`${BASE_URL}/api/admin/servicehosting/change_plan`, { + order_id: order_id, + plan_id: new_plan_id + }); + + +// ========================================== +// GESTION DES FACTURES +// ========================================== + +// Creation +// Suppression +// Modification +// Lister +export const getInvoiceList = () => + apiCall(`${BASE_URL}/api/client/invoice/get_list`); + +// Lire +export const getInvoiceDetails = (InvoiceHash) => + apiCall(`${BASE_URL}/api/client/invoice/get`); + + +// ========================================== +// GESTION DES TICKETS +// ========================================== + +// Creation +export const createTicket = (subject, message, helpdesk_id) => + apiCall(`${BASE_URL}/api/client/support/ticket_create`, { + support_helpdesk_id: helpdesk_id, + subject: subject, + content: message + }); +// Suppression + +// Lister les tickets +export const getClientTickets = () => + apiCall(`${BASE_URL}/api/client/support/ticket_get_list`); + +// Lister les helpdesks +export const getHelpdesks = () => + apiCall(`${BASE_URL}/api/client/support/helpdesk_get_pairs`); + +// Lire +export const getTicketDetails = (ticketId) => + apiCall(`${BASE_URL}/api/client/support/ticket_get`, { id: ticketId }); + +// Repondre +export const replyTicket = (ticketId, message) => + apiCall(`${BASE_URL}/api/client/support/ticket_reply`, { + id: ticketId, + content: message + }); \ No newline at end of file