""" Authentication module for SamCloud Dashboard Based on FileBrowser's secure authentication approach """ import sqlite3 import hashlib import secrets import os from datetime import datetime, timedelta from functools import wraps from flask import session, request, jsonify, redirect, url_for class AuthManager: def __init__(self, db_path='config/samcloud.db'): self.db_path = db_path self.init_database() def init_database(self): """Initialize the authentication database""" os.makedirs(os.path.dirname(self.db_path), exist_ok=True) conn = sqlite3.connect(self.db_path) cursor = conn.cursor() # Users table cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, salt TEXT NOT NULL, email TEXT, is_admin BOOLEAN DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP, permissions TEXT DEFAULT '{}', settings TEXT DEFAULT '{}' ) ''') # Sessions table cursor.execute(''' CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user_id INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP, ip_address TEXT, user_agent TEXT, FOREIGN KEY (user_id) REFERENCES users (id) ) ''') # Shared links table (for file sharing) cursor.execute(''' CREATE TABLE IF NOT EXISTS shared_links ( id TEXT PRIMARY KEY, user_id INTEGER, path TEXT NOT NULL, expires_at TIMESTAMP, password_hash TEXT, download_count INTEGER DEFAULT 0, max_downloads INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ) ''') conn.commit() conn.close() # Create default admin user if none exists self.create_default_admin() def create_default_admin(self): """Create default admin user if no users exist""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute('SELECT COUNT(*) FROM users') user_count = cursor.fetchone()[0] if user_count == 0: # Create admin user with secure random password admin_password = secrets.token_urlsafe(16) self.create_user('admin', admin_password, is_admin=True) # Write credentials to secure file with open('config/admin_credentials.txt', 'w') as f: f.write(f"Default Admin Credentials:\n") f.write(f"Username: admin\n") f.write(f"Password: {admin_password}\n") f.write(f"Created: {datetime.now()}\n") os.chmod('config/admin_credentials.txt', 0o600) # Read-only for owner print(f"Created default admin user. Credentials saved to config/admin_credentials.txt") conn.close() def hash_password(self, password, salt=None): """Hash password with salt""" if salt is None: salt = secrets.token_hex(32) password_hash = hashlib.pbkdf2_hmac( 'sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000 # iterations ) return password_hash.hex(), salt def create_user(self, username, password, email=None, is_admin=False): """Create a new user""" password_hash, salt = self.hash_password(password) conn = sqlite3.connect(self.db_path) cursor = conn.cursor() try: cursor.execute(''' INSERT INTO users (username, password_hash, salt, email, is_admin) VALUES (?, ?, ?, ?, ?) ''', (username, password_hash, salt, email, is_admin)) user_id = cursor.lastrowid conn.commit() return user_id except sqlite3.IntegrityError: return None # Username already exists finally: conn.close() def verify_user(self, username, password): """Verify user credentials""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(''' SELECT id, password_hash, salt, is_admin FROM users WHERE username = ? ''', (username,)) result = cursor.fetchone() conn.close() if result: user_id, stored_hash, salt, is_admin = result password_hash, _ = self.hash_password(password, salt) if password_hash == stored_hash: # Update last login self.update_last_login(user_id) return { 'id': user_id, 'username': username, 'is_admin': bool(is_admin) } return None def update_last_login(self, user_id): """Update user's last login timestamp""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(''' UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ? ''', (user_id,)) conn.commit() conn.close() def create_session(self, user_id, ip_address=None, user_agent=None): """Create a new session""" session_id = secrets.token_urlsafe(32) expires_at = datetime.now() + timedelta(days=7) # 7 day sessions conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(''' INSERT INTO sessions (id, user_id, expires_at, ip_address, user_agent) VALUES (?, ?, ?, ?, ?) ''', (session_id, user_id, expires_at, ip_address, user_agent)) conn.commit() conn.close() return session_id def verify_session(self, session_id): """Verify session and return user info""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(''' SELECT s.user_id, u.username, u.is_admin, s.expires_at FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.id = ? AND s.expires_at > CURRENT_TIMESTAMP ''', (session_id,)) result = cursor.fetchone() conn.close() if result: user_id, username, is_admin, expires_at = result return { 'id': user_id, 'username': username, 'is_admin': bool(is_admin) } return None def delete_session(self, session_id): """Delete a session (logout)""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute('DELETE FROM sessions WHERE id = ?', (session_id,)) conn.commit() conn.close() def create_shared_link(self, user_id, path, expires_hours=24, password=None, max_downloads=None): """Create a shared link for file/folder""" link_id = secrets.token_urlsafe(16) expires_at = datetime.now() + timedelta(hours=expires_hours) password_hash = None if password: password_hash, _ = self.hash_password(password) conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(''' INSERT INTO shared_links (id, user_id, path, expires_at, password_hash, max_downloads) VALUES (?, ?, ?, ?, ?, ?) ''', (link_id, user_id, path, expires_at, password_hash, max_downloads)) conn.commit() conn.close() return link_id # Authentication decorators def login_required(f): """Decorator to require authentication""" @wraps(f) def decorated_function(*args, **kwargs): # Skip auth in development mode if os.getenv('FLASK_ENV') == 'development' and os.getenv('SKIP_AUTH') == '1': return f(*args, **kwargs) session_id = session.get('session_id') if not session_id: if request.is_json: return jsonify({'error': 'Authentication required'}), 401 return redirect(url_for('login')) auth_manager = AuthManager() user = auth_manager.verify_session(session_id) if not user: session.clear() if request.is_json: return jsonify({'error': 'Session expired'}), 401 return redirect(url_for('login')) # Add user info to request context request.current_user = user return f(*args, **kwargs) return decorated_function def admin_required(f): """Decorator to require admin privileges""" @wraps(f) def decorated_function(*args, **kwargs): # Skip auth in development mode if os.getenv('FLASK_ENV') == 'development' and os.getenv('SKIP_AUTH') == '1': return f(*args, **kwargs) if not hasattr(request, 'current_user') or not request.current_user.get('is_admin'): if request.is_json: return jsonify({'error': 'Admin privileges required'}), 403 return redirect(url_for('login')) return f(*args, **kwargs) return decorated_function