Spaces:
Sleeping
Sleeping
| <html lang="fr" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Nexus | Clés API - Pro</title> | |
| <link rel="icon" type="image/png" href="https://i.imgur.com/7Gn3toV.png"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" | |
| href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" /> | |
| <style> | |
| /* Configuration de la police Inter */ | |
| body { font-family: 'Inter', sans-serif; } | |
| /* Transition pour un effet doux des couleurs */ | |
| .transition-colors-theme { transition: background-color 0.3s, color 0.3s, border-color 0.3s; } | |
| /* Style et positionnement du bouton de Thème */ | |
| .theme-switch { | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 24px; | |
| transition: color 0.2s; | |
| position: fixed; /* Reste fixe même si le reste défile */ | |
| top: 1rem; | |
| right: 1rem; | |
| z-index: 50; /* Doit être au-dessus de tout */ | |
| } | |
| /* Définir les Variables de Thème */ | |
| /* Mode Sombre (Défaut) */ | |
| :root, .dark { | |
| --bg-page: #121212; | |
| --bg-card: #1E1E1E; | |
| --bg-sidebar: #1E1E1E; /* Sidebar aussi foncée que les cartes */ | |
| --bg-sidebar-active: #2C2C2C; | |
| --border-sidebar: #383838; | |
| --text-color: #FFFFFF; | |
| --text-secondary: #D1D5DB; | |
| --text-link: #3B82F6; | |
| --text-link-hover: #60A5FA; | |
| --input-bg: #2C2C2C; | |
| --input-border: #4B5563; | |
| --input-text-key: #6EE7B7; /* Vert clair pour la clé */ | |
| --primary-color: #3B82F6; | |
| --primary-hover: #2563EB; | |
| --warning-bg: #451A03; /* Orange très sombre */ | |
| --warning-border: #78350F; | |
| --warning-text: #FBBF24; | |
| } | |
| /* Mode Clair */ | |
| .light { | |
| --bg-page: #F5F5F5; | |
| --bg-card: #FFFFFF; | |
| --bg-sidebar: #FFFFFF; | |
| --bg-sidebar-active: #F3F4F6; | |
| --border-sidebar: #D1D5DB; | |
| --text-color: #000000; | |
| --text-secondary: #4B5563; | |
| --text-link: #1D4ED8; | |
| --text-link-hover: #3B82F6; | |
| --input-bg: #FFFFFF; | |
| --input-border: #D1D5DB; | |
| --input-text-key: #059669; /* Vert foncé pour la clé */ | |
| --primary-color: #3B82F6; | |
| --primary-hover: #2563EB; | |
| --warning-bg: #FEF3C7; /* Jaune très clair */ | |
| --warning-border: #FBBF24; | |
| --warning-text: #92400E; | |
| } | |
| /* Appliquer les variables au body et aux éléments */ | |
| .bg-page-var { background-color: var(--bg-page); } | |
| .bg-card-var { background-color: var(--bg-card); } | |
| .bg-sidebar-var { background-color: var(--bg-sidebar); } | |
| .border-sidebar-var { border-color: var(--border-sidebar); } | |
| .text-color-var { color: var(--text-color); } | |
| .text-secondary-var { color: var(--text-secondary); } | |
| .text-link-var { color: var(--text-link); } | |
| .hover\:text-link-hover-var:hover { color: var(--text-link-hover); } | |
| .bg-input-var { background-color: var(--input-bg); } | |
| .border-input-var { border-color: var(--input-border); } | |
| .text-input-key-var { color: var(--input-text-key); } | |
| .bg-primary-color-var { background-color: var(--primary-color); } | |
| .hover\:bg-primary-hover-var:hover { background-color: var(--primary-hover); } | |
| .focus\:ring-primary-color-var:focus { --tw-ring-color: var(--primary-color); } | |
| .focus\:border-primary-color-var:focus { border-color: var(--primary-color); } | |
| .bg-sidebar-active-var { background-color: var(--bg-sidebar-active); } | |
| .bg-warning-var { background-color: var(--warning-bg); } | |
| .border-warning-var { border-color: var(--warning-border); } | |
| .text-warning-var { color: var(--warning-text); } | |
| /* Style pour les boutons et cartes carrés (Artboard) */ | |
| .btn-square, .card-square { | |
| border-radius: 0; /* Supprime l'arrondi */ | |
| } | |
| /* Styles spécifiques pour le chargement/spinner */ | |
| .spinner { | |
| border-top-color: var(--primary-color); /* Utiliser la couleur primaire pour le spinner */ | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* Style pour la Sidebar - FIXE et complète */ | |
| .sidebar { | |
| transition: transform 0.3s ease-in-out; | |
| transform: translateX(-100%); | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| z-index: 30; | |
| height: 100vh; | |
| } | |
| .sidebar.open { | |
| transform: translateX(0); | |
| } | |
| /* Sur les grands écrans (lg), la sidebar est statique et toujours visible */ | |
| @media (min-width: 1024px) { | |
| .sidebar { | |
| transform: translateX(0) ; | |
| position: fixed; /* Changement : Rendre la sidebar fixe sur grand écran */ | |
| height: 100vh; | |
| } | |
| } | |
| /* Conteneur principal pour décaler le contenu sur les grands écrans (lg) */ | |
| /* La marge doit correspondre à la largeur de la sidebar (w-64) */ | |
| @media (min-width: 1024px) { | |
| .main-content { | |
| margin-left: 256px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-page-var text-color-var min-h-screen transition-colors-theme"> | |
| <button id="theme-switch" class="theme-switch text-color-var hover:text-link-hover-var transition-colors-theme"> | |
| <span class="material-symbols-rounded">dark_mode</span> | |
| </button> | |
| <aside id="sidebar" class="sidebar w-64 bg-sidebar-var border-r border-sidebar-var p-6 lg:block transition-colors-theme"> | |
| <div class="flex items-center mb-10"> | |
| <img src="https://i.imgur.com/7Gn3toV.png" alt="Nexus Pro Logo" class="h-8"> | |
| <span class="text-xl font-bold text-color-var ml-2 transition-colors-theme">Nexus Pro</span> | |
| </div> | |
| <nav class="space-y-3"> | |
| <a href="{{ url_for('user_bp.dashboard') }}" class="flex items-center p-3 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">dashboard</span> | |
| <span class="ml-3">Dashboard</span> | |
| </a> | |
| <a href="{{ url_for('user_bp.api_key') }}" class="flex items-center p-3 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">key</span> | |
| <span class="ml-3">Clés API</span> | |
| </a> | |
| <a href="{{ url_for('user_bp.profile') }}" class="flex items-center p-3 bg-sidebar-active-var text-color-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">person</span> | |
| <span class="ml-3">Profil</span> | |
| </a> | |
| <a href="/api_logs" class="flex items-center p-3 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">list_alt</span> | |
| <span class="ml-3">Logs d'API</span> | |
| </a> | |
| <a href="/tarifs" class="flex items-center p-3 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">payments</span> | |
| <span class="ml-3">Tarifs</span> | |
| </a> | |
| <a href="/statut" class="flex items-center p-3 text-secondary-var hover:text-color-var hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">monitor_heart</span> | |
| <span class="ml-3">Statut de l'API</span> | |
| </a> | |
| <a href="{{ url_for('user_bp.deconnexion') }}" id="logout-button-sidebar" class="flex items-center p-3 text-red-500 hover:text-red-400 hover:bg-sidebar-active-var rounded-lg transition duration-150 transition-colors-theme"> | |
| <span class="material-symbols-rounded">logout</span> | |
| <span class="ml-3">Déconnexion</span> | |
| </a> | |
| </nav> | |
| <div class="absolute bottom-6 w-52 text-xs text-secondary-var transition-colors-theme"> | |
| <p>Connecté en tant que: <span class="font-semibold text-color-var transition-colors-theme">{{ user.email if user else 'Chargement...' }}</span></p> | |
| <p>Plan: <span class="font-semibold text-color-var transition-colors-theme">{{ (user.plan | upper) if user else 'Chargement...' }}</span></p> | |
| </div> | |
| </aside> | |
| <div id="main-content" class="main-content p-8 transition-colors-theme"> | |
| <header class="lg:hidden flex justify-between items-center mb-8"> | |
| <div class="flex items-center"> | |
| <img src="https://i.imgur.com/7Gn3toV.png" alt="Nexus Pro Logo" class="h-8"> | |
| <span class="text-xl font-bold text-color-var ml-2 transition-colors-theme">Nexus Pro</span> | |
| </div> | |
| <button id="sidebar-toggle" class="p-2 rounded-lg bg-sidebar-active-var hover:bg-sidebar-active-var/80 transition-colors-theme"> | |
| <span class="material-symbols-rounded text-color-var">menu</span> | |
| </button> | |
| </header> | |
| <h1 class="text-3xl font-bold mb-8 text-color-var transition-colors-theme">Gestion des Clés API</h1> | |
| {% with messages = get_flashed_messages(with_categories=true) %} | |
| {% if messages %} | |
| {% for category, message in messages %} | |
| <div class="p-4 mb-4 text-sm rounded-lg {{ 'bg-red-800 text-red-300' if category == 'error' or category == 'danger' else 'bg-green-800 text-green-300' }} border border-transparent" role="alert"> | |
| {{ message }} | |
| </div> | |
| {% endfor %} | |
| {% endif %} | |
| {% endwith %} | |
| <div id="security-warning-card" class="mb-8 p-4 bg-warning-var border border-warning-border-var rounded-lg text-warning-text-var transition-colors-theme relative"> | |
| <button id="dismiss-warning" class="absolute top-2 right-2 p-1 rounded-full text-warning-text-var hover:bg-warning-border-var/30 transition"> | |
| <span class="material-symbols-rounded text-lg">close</span> | |
| </button> | |
| <h3 class="font-bold flex items-center"> | |
| <span class="material-symbols-rounded mr-2">warning</span> | |
| Attention Sécurité | |
| </h3> | |
| <p class="text-sm mt-2">Chacune de vos 5 clés API est un mot de passe pour vos applications. Ne les partagez jamais publiquement et traitez-les comme des informations confidentielles. Régénérez immédiatement une clé si vous suspectez un risque.</p> | |
| </div> | |
| <div class="bg-card-var p-6 rounded-lg shadow-xl border border-sidebar-var transition-colors-theme"> | |
| <h2 class="text-xl font-semibold mb-4 text-color-var transition-colors-theme">Vos Clés d'Accès API (5 Clés Disponibles)</h2> | |
| <p class="text-secondary-var mb-6 transition-colors-theme">Chaque clé peut être utilisée pour authentifier vos requêtes auprès de l'API Nexus. Régénérez une clé si elle est compromise.</p> | |
| {% set api_keys = [user.get('api_key'), user.get('api_key_2'), user.get('api_key_3'), user.get('api_key_4'), user.get('api_key_5')] %} | |
| {% for key_value in api_keys %} | |
| {% set index = loop.index0 %} | |
| {% set display_value = key_value if key_value else 'Non générée (ou plan Free)' %} | |
| <div id="key-block-{{ index }}" class="key-block space-y-4 mb-4 p-4 border border-input-var rounded-lg bg-input-var/50 transition-colors-theme"> | |
| <h3 class="text-lg font-medium text-color-var transition-colors-theme">Clé API n°{{ index + 1 }}</h3> | |
| <div class="flex items-center space-x-4"> | |
| <input id="api-key-display-{{ index }}" | |
| type="text" | |
| readonly | |
| class="api-key-input flex-grow bg-input-var border border-input-var text-input-key-var p-3 rounded-lg font-mono text-sm break-all focus:outline-none focus:ring-2 focus:ring-primary-color-var focus:border-primary-color-var transition-colors-theme" | |
| value="{{ display_value }}"> | |
| <button data-key-index="{{ index }}" class="copy-btn bg-primary-color-var hover:bg-primary-hover-var text-white font-bold py-3 px-4 rounded-lg transition duration-200 flex items-center btn-square transition-colors-theme" | |
| data-clipboard-target="#api-key-display-{{ index }}"> | |
| <span class="material-symbols-rounded">content_copy</span> | |
| <span class="ml-2 hidden sm:inline">Copier</span> | |
| </button> | |
| </div> | |
| <div class="flex items-center justify-between border-t border-input-var pt-4 transition-colors-theme"> | |
| <button data-key-index="{{ index }}" class="regenerate-btn text-sm text-red-500 hover:text-red-400 transition duration-200 flex items-center font-medium transition-colors-theme" {% if not key_value %}disabled{% endif %}> | |
| <span class="material-symbols-rounded text-lg mr-1">refresh</span> | |
| Régénérer cette Clé | |
| <div id="spinner-{{ index }}" class="spinner w-4 h-4 border-2 rounded-full ml-2 hidden" style="border-bottom-color: transparent;"></div> | |
| </button> | |
| <p id="status-message-{{ index }}" class="status-message text-sm text-input-key-var font-medium transition-colors-theme"></p> | |
| </div> | |
| </div> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| <script> | |
| // 1. Logique du mode sombre/clair (CORRIGÉE) | |
| const themeSwitch = document.getElementById('theme-switch'); | |
| const htmlElement = document.documentElement; // Référence à l'élément <html> | |
| // Fonction pour appliquer le thème | |
| function applyTheme(theme) { | |
| // Ajoute la classe 'light' si le thème est 'light', sinon ajoute 'dark' | |
| if (theme === 'light') { | |
| htmlElement.classList.add('light'); | |
| htmlElement.classList.remove('dark'); | |
| // MÀJ de l'icône vers 'dark_mode' (lune) quand le mode est 'light' | |
| themeSwitch.innerHTML = '<span class="material-symbols-rounded">dark_mode</span>'; | |
| } else { | |
| htmlElement.classList.add('dark'); | |
| htmlElement.classList.remove('light'); | |
| // MÀJ de l'icône vers 'light_mode' (soleil) quand le mode est 'dark' | |
| themeSwitch.innerHTML = '<span class="material-symbols-rounded">light_mode</span>'; | |
| } | |
| localStorage.setItem('theme', theme); | |
| } | |
| // Initialisation du thème | |
| // Le thème par défaut est 'dark' si localStorage est vide ou si le système préfère le dark | |
| const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); | |
| applyTheme(savedTheme); | |
| // Événement de bascule | |
| themeSwitch.addEventListener('click', () => { | |
| // Détermine le nouveau thème basé sur la présence de la classe 'dark' sur <html> | |
| const currentTheme = htmlElement.classList.contains('dark') ? 'dark' : 'light'; | |
| const newTheme = currentTheme === 'light' ? 'dark' : 'light'; | |
| applyTheme(newTheme); | |
| }); | |
| // Logique de bascule de la sidebar pour mobile/tablette (CORRIGÉE : Ajout de la bascule 'open') | |
| const sidebarToggle = document.getElementById('sidebar-toggle'); | |
| const sidebar = document.getElementById('sidebar'); | |
| // Le mainContent n'a pas besoin de la classe 'sidebar-open' pour le style actuel | |
| // const mainContent = document.getElementById('main-content'); | |
| if (sidebarToggle && sidebar) { | |
| sidebarToggle.addEventListener('click', () => { | |
| sidebar.classList.toggle('open'); // Bascule de la classe 'open' | |
| }); | |
| } | |
| // FIN de la Logique du mode sombre/clair et Sidebar | |
| // --- Logique d'affichage et d'interaction des Clés API (MISE À JOUR) --- | |
| // 2. Logique d'avertissement de sécurité (conservée) | |
| const warningCard = document.getElementById('security-warning-card'); | |
| const dismissButton = document.getElementById('dismiss-warning'); | |
| if (warningCard && dismissButton) { | |
| // Afficher si non masqué précédemment | |
| if (localStorage.getItem('dismissed_warning') !== 'true') { | |
| warningCard.style.display = 'block'; | |
| } else { | |
| warningCard.style.display = 'none'; | |
| } | |
| dismissButton.addEventListener('click', () => { | |
| warningCard.style.display = 'none'; | |
| // Stocker dans le localStorage pour masquer lors des prochaines visites | |
| localStorage.setItem('dismissed_warning', 'true'); | |
| }); | |
| } | |
| // 3. Logique de Copie (MISE À JOUR) | |
| const copyButtons = document.querySelectorAll('.copy-btn'); | |
| copyButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| // Récupère l'ID de l'input à copier | |
| const targetId = button.getAttribute('data-clipboard-target').substring(1); | |
| const apiKeyInput = document.getElementById(targetId); | |
| const apiKey = apiKeyInput.value.trim(); | |
| const index = button.getAttribute('data-key-index'); | |
| const statusMessage = document.getElementById(`status-message-${index}`); | |
| if (apiKey === 'Non générée (ou plan Free)' || apiKey.includes('NON_DISPONIBLE')) { | |
| statusMessage.textContent = 'Erreur: Aucune clé à copier.'; | |
| setTimeout(() => { statusMessage.textContent = ''; }, 3000); | |
| return; | |
| } | |
| // Copie au presse-papiers | |
| navigator.clipboard.writeText(apiKey).then(() => { | |
| statusMessage.textContent = 'Clé API copiée !'; | |
| statusMessage.classList.add('text-green-500'); | |
| setTimeout(() => { | |
| statusMessage.textContent = ''; | |
| statusMessage.classList.remove('text-green-500'); | |
| }, 3000); | |
| }).catch(err => { | |
| statusMessage.textContent = 'Échec de la copie.'; | |
| statusMessage.classList.add('text-red-500'); | |
| console.error('Échec de la copie:', err); | |
| setTimeout(() => { | |
| statusMessage.textContent = ''; | |
| statusMessage.classList.remove('text-red-500'); | |
| }, 3000); | |
| }); | |
| }); | |
| }); | |
| // 4. Logique de Régénération (MISE À JOUR) | |
| const regenerateButtons = document.querySelectorAll('.regenerate-btn'); | |
| regenerateButtons.forEach(button => { | |
| button.addEventListener('click', async (e) => { | |
| e.preventDefault(); | |
| const index = button.getAttribute('data-key-index'); // Récupère l'index (0 à 4) | |
| const apiKeyInput = document.getElementById(`api-key-display-${index}`); | |
| const statusMessage = document.getElementById(`status-message-${index}`); | |
| const spinner = document.getElementById(`spinner-${index}`); | |
| // Affichage de l'indicateur de chargement | |
| statusMessage.textContent = 'Régénération en cours...'; | |
| statusMessage.classList.remove('text-green-500', 'text-red-500'); | |
| statusMessage.classList.add('text-input-key-var'); | |
| spinner.classList.remove('hidden'); | |
| button.disabled = true; | |
| try { | |
| // Envoi de l'index de la clé (0 à 4) au backend Flask | |
| const response = await fetch('/api/regenerate-api-key', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ key_index: parseInt(index) }) | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| // MÀJ de la valeur affichée avec la nouvelle clé | |
| apiKeyInput.value = data.new_key; | |
| statusMessage.textContent = data.message; | |
| statusMessage.classList.add('text-green-500'); | |
| button.disabled = false; // Réactive le bouton | |
| } else { | |
| statusMessage.textContent = `Erreur: ${data.message}`; | |
| statusMessage.classList.add('text-red-500'); | |
| // Le bouton reste désactivé ou est réactivé selon l'erreur | |
| if (apiKeyInput.value.includes('NON_DISPONIBLE')) { | |
| button.disabled = true; | |
| } else { | |
| button.disabled = false; | |
| } | |
| } | |
| } catch (error) { | |
| statusMessage.textContent = 'Erreur réseau lors de la régénération.'; | |
| statusMessage.classList.add('text-red-500'); | |
| console.error('Erreur de régénération:', error); | |
| button.disabled = false; | |
| } finally { | |
| spinner.classList.add('hidden'); | |
| // Nettoyage du message de statut après un délai | |
| setTimeout(() => { | |
| statusMessage.textContent = ''; | |
| statusMessage.classList.remove('text-green-500', 'text-red-500', 'text-input-key-var'); | |
| }, 4000); | |
| } | |
| }); | |
| }); | |
| // 5. Logique de Déconnexion (Conservée) | |
| const logoutHandler = async (e) => { | |
| e.preventDefault(); | |
| // La déconnexion est maintenant gérée par la route Flask directe | |
| window.location.href = document.getElementById('logout-button-sidebar').href; | |
| }; | |
| // On attache l'événement à la déconnexion dans la sidebar. | |
| document.getElementById('logout-button-sidebar').addEventListener('click', logoutHandler); | |
| </script> |