Nexus / templates /api_key.html
ernestmindres's picture
Update templates/api_key.html
2f67c75 verified
<!DOCTYPE html>
<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) !important;
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>