diff --git a/ConfluentTranslator/.env.example b/ConfluentTranslator/.env.example new file mode 100644 index 0000000..b38f069 --- /dev/null +++ b/ConfluentTranslator/.env.example @@ -0,0 +1,14 @@ +# ConfluentTranslator Configuration + +# Server +PORT=3000 + +# API Keys (LLM) +ANTHROPIC_API_KEY=sk-ant-your-key-here +OPENAI_API_KEY=sk-your-key-here + +# Security (optionnel - utilisé pour JWT, peut être généré aléatoirement) +JWT_SECRET=changez-ce-secret-en-production + +# Note: Les API keys pour le traducteur (authentication) sont gérées dans data/tokens.json +# Le token admin sera automatiquement créé au premier lancement et affiché dans les logs diff --git a/ConfluentTranslator/adminRoutes.js b/ConfluentTranslator/adminRoutes.js new file mode 100644 index 0000000..99cb4c8 --- /dev/null +++ b/ConfluentTranslator/adminRoutes.js @@ -0,0 +1,95 @@ +const express = require('express'); +const router = express.Router(); +const { requireAdmin, createToken, listTokens, disableToken, enableToken, deleteToken, getGlobalStats } = require('./auth'); +const { getLogs, getLogStats } = require('./logger'); +const { adminLimiter } = require('./rateLimiter'); + +// Appliquer l'auth et rate limiting à toutes les routes admin +router.use(requireAdmin); +router.use(adminLimiter); + +// Liste des tokens +router.get('/tokens', (req, res) => { + const tokens = listTokens(); + res.json({ tokens }); +}); + +// Créer un nouveau token +router.post('/tokens', (req, res) => { + const { name, role = 'user', dailyLimit = 100 } = req.body; + + if (!name) { + return res.status(400).json({ error: 'Missing parameter: name' }); + } + + const token = createToken(name, role, dailyLimit); + res.json({ + success: true, + token: { + ...token, + apiKey: token.apiKey // Retourner la clé complète seulement à la création + } + }); +}); + +// Désactiver un token +router.post('/tokens/:id/disable', (req, res) => { + const { id } = req.params; + const success = disableToken(id); + + if (success) { + res.json({ success: true, message: 'Token disabled' }); + } else { + res.status(404).json({ error: 'Token not found' }); + } +}); + +// Réactiver un token +router.post('/tokens/:id/enable', (req, res) => { + const { id } = req.params; + const success = enableToken(id); + + if (success) { + res.json({ success: true, message: 'Token enabled' }); + } else { + res.status(404).json({ error: 'Token not found' }); + } +}); + +// Supprimer un token +router.delete('/tokens/:id', (req, res) => { + const { id } = req.params; + const success = deleteToken(id); + + if (success) { + res.json({ success: true, message: 'Token deleted' }); + } else { + res.status(404).json({ error: 'Token not found or cannot be deleted' }); + } +}); + +// Stats globales +router.get('/stats', (req, res) => { + const tokenStats = getGlobalStats(); + const logStats = getLogStats(); + + res.json({ + tokens: tokenStats, + logs: logStats + }); +}); + +// Logs +router.get('/logs', (req, res) => { + const { limit = 100, user, path, statusCode } = req.query; + + const filter = {}; + if (user) filter.user = user; + if (path) filter.path = path; + if (statusCode) filter.statusCode = parseInt(statusCode); + + const logs = getLogs(parseInt(limit), filter); + res.json({ logs }); +}); + +module.exports = router; diff --git a/ConfluentTranslator/auth.js b/ConfluentTranslator/auth.js new file mode 100644 index 0000000..171a57a --- /dev/null +++ b/ConfluentTranslator/auth.js @@ -0,0 +1,206 @@ +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); +const { v4: uuidv4 } = require('uuid'); +const fs = require('fs'); +const path = require('path'); + +const TOKENS_FILE = path.join(__dirname, 'data', 'tokens.json'); +const JWT_SECRET = process.env.JWT_SECRET || 'confluent-secret-key-change-in-production'; + +// Structure des tokens +let tokens = {}; + +function loadTokens() { + try { + const dataDir = path.join(__dirname, 'data'); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + + if (fs.existsSync(TOKENS_FILE)) { + return JSON.parse(fs.readFileSync(TOKENS_FILE, 'utf8')); + } + } catch (error) { + console.error('Error loading tokens:', error); + } + + // Default: créer un token admin si aucun token n'existe + const defaultTokens = { + admin: { + id: 'admin', + name: 'Admin', + role: 'admin', + apiKey: uuidv4(), + createdAt: new Date().toISOString(), + active: true, + requestsToday: 0, + dailyLimit: -1 // illimité + } + }; + + saveTokens(defaultTokens); + console.log('🔑 Token admin créé:', defaultTokens.admin.apiKey); + return defaultTokens; +} + +function saveTokens() { + try { + fs.writeFileSync(TOKENS_FILE, JSON.stringify(tokens, null, 2)); + } catch (error) { + console.error('Error saving tokens:', error); + } +} + +// Middleware d'authentification +function authenticate(req, res, next) { + // Routes publiques (GET seulement) + const publicRoutes = ['/api/lexique', '/api/stats', '/lexique']; + if (req.method === 'GET' && publicRoutes.some(route => req.path.startsWith(route))) { + return next(); + } + + const apiKey = req.headers['x-api-key'] || req.query.apiKey; + + if (!apiKey) { + return res.status(401).json({ error: 'API key required' }); + } + + // Chercher le token + const token = Object.values(tokens).find(t => t.apiKey === apiKey); + + if (!token) { + return res.status(401).json({ error: 'Invalid API key' }); + } + + if (!token.active) { + return res.status(403).json({ error: 'Token disabled' }); + } + + // Vérifier la limite quotidienne + const today = new Date().toISOString().split('T')[0]; + const tokenToday = token.lastUsed?.split('T')[0]; + + if (tokenToday !== today) { + token.requestsToday = 0; + } + + if (token.dailyLimit > 0 && token.requestsToday >= token.dailyLimit) { + return res.status(429).json({ error: 'Daily limit reached' }); + } + + // Mettre à jour les stats + token.requestsToday++; + token.lastUsed = new Date().toISOString(); + saveTokens(); + + // Ajouter les infos au req + req.user = { + id: token.id, + name: token.name, + role: token.role + }; + + next(); +} + +// Middleware admin uniquement +function requireAdmin(req, res, next) { + if (!req.user || req.user.role !== 'admin') { + return res.status(403).json({ error: 'Admin access required' }); + } + next(); +} + +// Créer un nouveau token +function createToken(name, role = 'user', dailyLimit = 100) { + const id = uuidv4(); + const apiKey = uuidv4(); + + tokens[id] = { + id, + name, + role, + apiKey, + createdAt: new Date().toISOString(), + active: true, + requestsToday: 0, + dailyLimit + }; + + saveTokens(); + return tokens[id]; +} + +// Lister tous les tokens +function listTokens() { + return Object.values(tokens).map(t => ({ + id: t.id, + name: t.name, + role: t.role, + apiKey: t.apiKey.substring(0, 8) + '...', + createdAt: t.createdAt, + active: t.active, + requestsToday: t.requestsToday, + dailyLimit: t.dailyLimit, + lastUsed: t.lastUsed + })); +} + +// Désactiver un token +function disableToken(id) { + if (tokens[id]) { + tokens[id].active = false; + saveTokens(); + return true; + } + return false; +} + +// Réactiver un token +function enableToken(id) { + if (tokens[id]) { + tokens[id].active = true; + saveTokens(); + return true; + } + return false; +} + +// Supprimer un token +function deleteToken(id) { + if (id === 'admin') { + return false; // Ne pas supprimer l'admin + } + if (tokens[id]) { + delete tokens[id]; + saveTokens(); + return true; + } + return false; +} + +// Stats globales +function getGlobalStats() { + const tokenList = Object.values(tokens); + return { + totalTokens: tokenList.length, + activeTokens: tokenList.filter(t => t.active).length, + totalRequestsToday: tokenList.reduce((sum, t) => sum + t.requestsToday, 0) + }; +} + +// Charger les tokens au démarrage +tokens = loadTokens(); + +module.exports = { + authenticate, + requireAdmin, + createToken, + listTokens, + disableToken, + enableToken, + deleteToken, + getGlobalStats, + loadTokens, + tokens +}; diff --git a/ConfluentTranslator/data/tokens.json b/ConfluentTranslator/data/tokens.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/ConfluentTranslator/data/tokens.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/ConfluentTranslator/logger.js b/ConfluentTranslator/logger.js new file mode 100644 index 0000000..69aecd0 --- /dev/null +++ b/ConfluentTranslator/logger.js @@ -0,0 +1,151 @@ +const fs = require('fs'); +const path = require('path'); + +const LOGS_DIR = path.join(__dirname, 'logs'); +const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10 MB + +// Créer le dossier logs s'il n'existe pas +if (!fs.existsSync(LOGS_DIR)) { + fs.mkdirSync(LOGS_DIR, { recursive: true }); +} + +function getLogFile() { + const today = new Date().toISOString().split('T')[0]; + return path.join(LOGS_DIR, `requests-${today}.log`); +} + +function log(type, data) { + const timestamp = new Date().toISOString(); + const logEntry = { + timestamp, + type, + ...data + }; + + const logFile = getLogFile(); + const logLine = JSON.stringify(logEntry) + '\n'; + + fs.appendFileSync(logFile, logLine); + + // Rotation si le fichier devient trop gros + try { + const stats = fs.statSync(logFile); + if (stats.size > MAX_LOG_SIZE) { + const archiveName = logFile.replace('.log', `-${Date.now()}.log`); + fs.renameSync(logFile, archiveName); + } + } catch (error) { + console.error('Error rotating log file:', error); + } +} + +// Middleware de logging +function requestLogger(req, res, next) { + const start = Date.now(); + + // Capturer la réponse + const originalSend = res.send; + res.send = function (data) { + const duration = Date.now() - start; + + log('request', { + method: req.method, + path: req.path, + ip: req.ip || req.connection.remoteAddress, + user: req.user?.name || 'anonymous', + userId: req.user?.id || null, + statusCode: res.statusCode, + duration, + userAgent: req.headers['user-agent'] + }); + + originalSend.apply(res, arguments); + }; + + next(); +} + +// Lire les logs +function getLogs(limit = 100, filter = {}) { + const logFile = getLogFile(); + + if (!fs.existsSync(logFile)) { + return []; + } + + const logs = fs.readFileSync(logFile, 'utf8') + .split('\n') + .filter(line => line.trim()) + .map(line => { + try { + return JSON.parse(line); + } catch (e) { + return null; + } + }) + .filter(log => log !== null); + + // Appliquer les filtres + let filtered = logs; + + if (filter.user) { + filtered = filtered.filter(log => log.user === filter.user); + } + + if (filter.path) { + filtered = filtered.filter(log => log.path && log.path.includes(filter.path)); + } + + if (filter.statusCode) { + filtered = filtered.filter(log => log.statusCode === filter.statusCode); + } + + // Retourner les derniers logs + return filtered.slice(-limit).reverse(); +} + +// Stats des logs +function getLogStats() { + const logs = getLogs(1000); + + const stats = { + totalRequests: logs.length, + byUser: {}, + byPath: {}, + byStatus: {}, + avgDuration: 0, + errors: 0 + }; + + let totalDuration = 0; + + logs.forEach(log => { + // Par utilisateur + stats.byUser[log.user] = (stats.byUser[log.user] || 0) + 1; + + // Par path + stats.byPath[log.path] = (stats.byPath[log.path] || 0) + 1; + + // Par status + stats.byStatus[log.statusCode] = (stats.byStatus[log.statusCode] || 0) + 1; + + // Durée + totalDuration += log.duration || 0; + + // Erreurs + if (log.statusCode >= 400) { + stats.errors++; + } + }); + + stats.avgDuration = logs.length > 0 ? Math.round(totalDuration / logs.length) : 0; + + return stats; +} + +module.exports = { + log, + requestLogger, + getLogs, + getLogStats +}; diff --git a/ConfluentTranslator/package-lock.json b/ConfluentTranslator/package-lock.json index ba53375..d275a54 100644 --- a/ConfluentTranslator/package-lock.json +++ b/ConfluentTranslator/package-lock.json @@ -9,9 +9,13 @@ "version": "1.0.0", "dependencies": { "@anthropic-ai/sdk": "^0.71.0", + "bcryptjs": "^3.0.3", "dotenv": "^16.3.1", "express": "^4.18.2", - "openai": "^4.20.1" + "express-rate-limit": "^8.2.1", + "jsonwebtoken": "^9.0.2", + "openai": "^4.20.1", + "uuid": "^13.0.0" }, "devDependencies": { "nodemon": "^3.0.1" @@ -135,6 +139,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -196,6 +209,12 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -377,6 +396,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -507,6 +535,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -785,6 +831,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -853,6 +908,97 @@ "node": ">=16" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1238,7 +1384,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1502,6 +1647,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/ConfluentTranslator/package.json b/ConfluentTranslator/package.json index f792fd2..6dc1931 100644 --- a/ConfluentTranslator/package.json +++ b/ConfluentTranslator/package.json @@ -9,9 +9,13 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.71.0", + "bcryptjs": "^3.0.3", "dotenv": "^16.3.1", "express": "^4.18.2", - "openai": "^4.20.1" + "express-rate-limit": "^8.2.1", + "jsonwebtoken": "^9.0.2", + "openai": "^4.20.1", + "uuid": "^13.0.0" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/ConfluentTranslator/public/index.html b/ConfluentTranslator/public/index.html index 794893e..1e73b26 100644 --- a/ConfluentTranslator/public/index.html +++ b/ConfluentTranslator/public/index.html @@ -415,11 +415,110 @@ color: #666; border-bottom-color: #e0e0e0; } + + /* Login overlay */ + .login-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.95); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + } + .login-overlay.hidden { + display: none; + } + .login-modal { + background: #2a2a2a; + border: 2px solid #4a9eff; + border-radius: 12px; + padding: 40px; + max-width: 500px; + width: 90%; + box-shadow: 0 10px 40px rgba(74, 158, 255, 0.3); + } + .login-modal h2 { + color: #4a9eff; + margin-bottom: 20px; + font-size: 1.5em; + text-align: center; + } + .login-modal p { + color: #b0b0b0; + margin-bottom: 20px; + line-height: 1.6; + } + .login-modal .form-group { + margin-bottom: 20px; + } + .login-modal input { + width: 100%; + padding: 12px; + font-size: 1em; + font-family: monospace; + } + .login-modal button { + width: 100%; + padding: 14px; + font-size: 1.1em; + } + .login-error { + color: #ff4a4a; + margin-top: 10px; + text-align: center; + display: none; + } + body.light-theme .login-modal { + background: #ffffff; + border-color: #2563eb; + box-shadow: 0 10px 40px rgba(37, 99, 235, 0.3); + } + body.light-theme .login-modal h2 { + color: #2563eb; + } + + /* Logout button */ + .header-container { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + } + .logout-btn { + background: #555; + padding: 8px 16px; + font-size: 0.9em; + font-weight: normal; + } + .logout-btn:hover { + background: #666; + }
+ + +