Spaces:
Paused
Paused
| // auth.js | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const crypto = require('crypto'); | |
| const Database = require('better-sqlite3'); | |
| const bcrypt = require('bcrypt'); | |
| const jwt = require('jsonwebtoken'); | |
| // --- LOGGER BACKEND (Structured JSON for HF Logs) --- | |
| const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; | |
| const levels = { debug: 0, info: 1, warn: 2, error: 3 }; | |
| function log(level, msg, meta = {}) { | |
| if (levels[level] < levels[LOG_LEVEL]) return; | |
| console.log(JSON.stringify({ ts: new Date().toISOString(), level: level.toUpperCase(), msg, ...meta })); | |
| } | |
| const DATA_ROOT = '/data'; | |
| const META_DB_PATH = path.join(DATA_ROOT, 'meta.sqlite'); | |
| const JWT_SECRET = process.env.JWT_SECRET || 'change_me_in_hf_secrets_please'; | |
| const GLOBAL_QUOTA_BYTES = 5 * 1024 * 1024 * 1024; | |
| const TOS_VERSION = 1; | |
| const metaDb = new Database(META_DB_PATH); | |
| metaDb.pragma('journal_mode = WAL'); | |
| metaDb.exec(` | |
| CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, email TEXT UNIQUE, password_hash TEXT, created_at INTEGER, totp_secret TEXT, accepted_tos INTEGER DEFAULT 0); | |
| CREATE TABLE IF NOT EXISTS databases (id TEXT PRIMARY KEY, owner_id TEXT, name TEXT, created_at INTEGER, size_bytes INTEGER DEFAULT 0, FOREIGN KEY(owner_id) REFERENCES users(id)); | |
| CREATE INDEX IF NOT EXISTS idx_dbs_owner ON databases(owner_id); | |
| `); | |
| try { metaDb.prepare('ALTER TABLE users ADD COLUMN accepted_tos INTEGER DEFAULT 0').run(); } catch(e) {} | |
| // --- CAPTCHA --- | |
| function generateCaptcha() { | |
| const a = Math.floor(Math.random() * 10) + 1, b = Math.floor(Math.random() * 10) + 1; | |
| const op = Math.random() > 0.5 ? '+' : '-'; | |
| const q = `${a} ${op} ${b}`, ans = op === '+' ? a + b : a - b; | |
| const token = crypto.randomBytes(16).toString('hex'); | |
| if (!global.captchaStore) global.captchaStore = new Map(); | |
| global.captchaStore.set(token, { answer: ans, expires: Date.now() + 300000 }); | |
| log('debug', 'Captcha generated', { token: token.slice(0,6) }); | |
| return { token, question: q }; | |
| } | |
| function verifyCaptcha(token, userAnswer) { | |
| const store = global.captchaStore || new Map(); | |
| const data = store.get(token); | |
| if (!data || Date.now() > data.expires) { if(data) store.delete(token); log('warn', 'Captcha expired/invalid', { token: token?.slice(0,6) }); return false; } | |
| const ok = parseInt(userAnswer) === data.answer; | |
| if (ok) store.delete(token); | |
| log('debug', 'Captcha verified', { success: ok, token: token?.slice(0,6) }); | |
| return ok; | |
| } | |
| // --- UTILS --- | |
| function getDirSizeSync(dir) { if (!fs.existsSync(dir)) return 0; let s=0; for(const f of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,f.name); s+=f.isDirectory()?getDirSizeSync(p):fs.statSync(p).size;} return s; } | |
| function getGlobalUsed() { return getDirSizeSync(DATA_ROOT); } | |
| // --- AUTH --- | |
| function registerUser(email, password, captchaToken, captchaAnswer, acceptTos) { | |
| log('info', 'Register attempt', { email }); | |
| if (!verifyCaptcha(captchaToken, captchaAnswer)) { log('warn', 'Register fail: captcha'); throw new Error('Captcha invalide/expiré'); } | |
| if (!acceptTos) { log('warn', 'Register fail: tos'); throw new Error('Vous devez accepter les CGU'); } | |
| if (metaDb.prepare('SELECT id FROM users WHERE email = ?').get(email)) { log('warn', 'Register fail: exists', { email }); throw new Error('Email déjà utilisé'); } | |
| const id = crypto.randomUUID(), hash = bcrypt.hashSync(password, 10); | |
| metaDb.prepare('INSERT INTO users (id, email, password_hash, created_at, accepted_tos) VALUES (?, ?, ?, ?, ?)').run(id, email, hash, Date.now(), TOS_VERSION); | |
| log('info', 'User registered', { uid: id, email }); | |
| return { id, email }; | |
| } | |
| function loginUser(email, password) { | |
| log('info', 'Login attempt', { email }); | |
| const user = metaDb.prepare('SELECT * FROM users WHERE email = ?').get(email); | |
| if (!user || !bcrypt.compareSync(password, user.password_hash)) { log('warn', 'Login fail: creds', { email }); throw new Error('Identifiants invalides'); } | |
| if (user.accepted_tos < TOS_VERSION) { log('info', 'Login: TOS required', { uid: user.id, current: user.accepted_tos, required: TOS_VERSION }); return { tosRequired: true, tempUser: { id: user.id, email: user.email } }; } | |
| const token = jwt.sign({ uid: user.id, email: user.email }, JWT_SECRET, { expiresIn: '30d' }); | |
| log('info', 'Login success', { uid: user.id }); | |
| return { token, user: { id: user.id, email: user.email } }; | |
| } | |
| function acceptTos(userId) { | |
| log('info', 'TOS accepted', { uid: userId }); | |
| metaDb.prepare('UPDATE users SET accepted_tos = ? WHERE id = ?').run(TOS_VERSION, userId); | |
| const user = metaDb.prepare('SELECT id, email FROM users WHERE id = ?').get(userId); | |
| return { token: jwt.sign({ uid: user.id, email: user.email }, JWT_SECRET, { expiresIn: '30d' }), user: { id: user.id, email: user.email } }; | |
| } | |
| function verifyToken(token) { try { return jwt.verify(token, JWT_SECRET); } catch (e) { log('debug', 'Token verify fail', { err: e.message }); return null; } } | |
| // --- DATABASES --- | |
| function createDatabase(ownerId, dbName, captchaToken, captchaAnswer) { | |
| log('info', 'Create DB attempt', { ownerId, name: dbName }); | |
| if (!verifyCaptcha(captchaToken, captchaAnswer)) { log('warn', 'Create DB fail: captcha'); throw new Error('Captcha invalide/expiré'); } | |
| if (getGlobalUsed() >= GLOBAL_QUOTA_BYTES) { log('error', 'Create DB fail: global quota'); throw new Error('Quota global 5Go atteint'); } | |
| const safeName = dbName.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 50); | |
| if (!safeName) throw new Error('Nom invalide'); | |
| const dbId = crypto.randomUUID(), dbPath = path.join(DATA_ROOT, ownerId, `${dbId}.sqlite`); | |
| fs.mkdirSync(path.dirname(dbPath), { recursive: true }); | |
| const newDb = new Database(dbPath); newDb.exec('PRAGMA journal_mode = WAL; CREATE TABLE IF NOT EXISTS _meta (k TEXT PRIMARY KEY, v TEXT);'); newDb.close(); | |
| metaDb.prepare('INSERT INTO databases (id, owner_id, name, created_at, size_bytes) VALUES (?, ?, ?, ?, 0)').run(dbId, ownerId, safeName, Date.now()); | |
| log('info', 'Database created', { dbId, ownerId, name: safeName }); | |
| return { id: dbId, name: safeName }; | |
| } | |
| function listDatabases(ownerId) { return metaDb.prepare('SELECT id, name, created_at, size_bytes FROM databases WHERE owner_id = ? ORDER BY created_at DESC').all(ownerId); } | |
| function getDatabaseFilePath(ownerId, dbId) { const row = metaDb.prepare('SELECT id FROM databases WHERE id = ? AND owner_id = ?').get(dbId, ownerId); return row ? path.join(DATA_ROOT, ownerId, `${dbId}.sqlite`) : null; } | |
| function deleteDatabase(ownerId, dbId) { log('warn', 'Delete DB', { ownerId, dbId }); const fp = getDatabaseFilePath(ownerId, dbId); if (fp && fs.existsSync(fp)) fs.unlinkSync(fp); metaDb.prepare('DELETE FROM databases WHERE id = ? AND owner_id = ?').run(dbId, ownerId); } | |
| function updateDbSize(dbId) { const row = metaDb.prepare('SELECT owner_id FROM databases WHERE id = ?').get(dbId); if(!row) return 0; const fp = path.join(DATA_ROOT, row.owner_id, `${dbId}.sqlite`); const size = fs.existsSync(fp) ? fs.statSync(fp).size : 0; metaDb.prepare('UPDATE databases SET size_bytes = ? WHERE id = ?').run(size, dbId); return size; } | |
| function getUserStats(ownerId) { const dbs = metaDb.prepare('SELECT SUM(size_bytes) as total, COUNT(*) as count FROM databases WHERE owner_id = ?').get(ownerId); return { totalBytes: dbs.total || 0, count: dbs.count || 0 }; } | |
| function executeSql(ownerId, dbId, sql, params = []) { | |
| const fp = getDatabaseFilePath(ownerId, dbId); | |
| if (!fp || !fs.existsSync(fp)) { log('error', 'SQL Exec: DB not found', { ownerId, dbId }); throw new Error('DB introuvable'); } | |
| const isSelect = sql.trim().toUpperCase().startsWith('SELECT') || sql.trim().toUpperCase().startsWith('PRAGMA'); | |
| log('debug', 'SQL Exec', { ownerId, dbId, type: isSelect ? 'READ' : 'WRITE', sql: sql.substring(0, 100) }); | |
| const db = new Database(fp, { readonly: false }); | |
| try { | |
| const stmt = db.prepare(sql); | |
| const result = isSelect ? stmt.all(...params) : stmt.run(...params); | |
| updateDbSize(dbId); | |
| return { success: true, data: result, changes: stmt.changes }; | |
| } catch (e) { log('error', 'SQL Error', { sql: sql.substring(0,100), err: e.message }); return { success: false, error: e.message }; } | |
| finally { db.close(); } | |
| } | |
| module.exports = { generateCaptcha, verifyCaptcha, TOS_VERSION, registerUser, loginUser, acceptTos, verifyToken, createDatabase, listDatabases, getDatabaseFilePath, deleteDatabase, executeSql, getUserStats, getGlobalUsed, GLOBAL_QUOTA_BYTES }; |