From 1554723ed46250baa8036a50eb9d987f838ec47a Mon Sep 17 00:00:00 2001 From: florianuhlig Date: Fri, 3 Oct 2025 15:03:18 +0200 Subject: [PATCH] To_DO: ERROR AFTER LOGGING IN --- .gitignore | 2 + {sqlLite => OLD/sqlLite}/__init__.py | 0 {sqlLite => OLD/sqlLite}/auth.py | 0 {sqlLite => OLD/sqlLite}/create.py | 0 {sqlLite => OLD/sqlLite}/get.py | 0 {sqlLite => OLD/sqlLite}/set.py | 0 {standard => OLD/standard}/getter.py | 0 config/database.py | 46 ++++ database/__init__.py | 40 +++ database/flask_integration.py | 38 +++ database/interface.py | 40 +++ database/mysql_db.py | 0 database/postgresql_db.py | 0 database/sqlite_db.py | 189 +++++++++++++ frontend/app.py | 276 ++++++++++++++++--- frontend/templates/dashboard.html | 397 ++++++++++++++++++++------- frontend/templates/login.html | 295 +++++++++++--------- frontend/templates/profile.html | 155 +++++++++++ main.py | 30 +- models/user.py | 0 services/__init__.py | 0 services/auth_service.py | 50 ++++ services/user_service.py | 73 +++++ utils/__init__.py | 0 utils/auth_decorators.py | 43 +++ utils/password_utils.py | 27 ++ utils/validation.py | 56 ++++ 27 files changed, 1484 insertions(+), 273 deletions(-) rename {sqlLite => OLD/sqlLite}/__init__.py (100%) rename {sqlLite => OLD/sqlLite}/auth.py (100%) rename {sqlLite => OLD/sqlLite}/create.py (100%) rename {sqlLite => OLD/sqlLite}/get.py (100%) rename {sqlLite => OLD/sqlLite}/set.py (100%) rename {standard => OLD/standard}/getter.py (100%) create mode 100644 config/database.py create mode 100644 database/__init__.py create mode 100644 database/flask_integration.py create mode 100644 database/interface.py create mode 100644 database/mysql_db.py create mode 100644 database/postgresql_db.py create mode 100644 database/sqlite_db.py create mode 100644 frontend/templates/profile.html create mode 100644 models/user.py create mode 100644 services/__init__.py create mode 100644 services/auth_service.py create mode 100644 services/user_service.py create mode 100644 utils/__init__.py create mode 100644 utils/auth_decorators.py create mode 100644 utils/password_utils.py create mode 100644 utils/validation.py diff --git a/.gitignore b/.gitignore index 9b0ee44..d4e940c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /.idea/ testing.py /databases/ + +.env \ No newline at end of file diff --git a/sqlLite/__init__.py b/OLD/sqlLite/__init__.py similarity index 100% rename from sqlLite/__init__.py rename to OLD/sqlLite/__init__.py diff --git a/sqlLite/auth.py b/OLD/sqlLite/auth.py similarity index 100% rename from sqlLite/auth.py rename to OLD/sqlLite/auth.py diff --git a/sqlLite/create.py b/OLD/sqlLite/create.py similarity index 100% rename from sqlLite/create.py rename to OLD/sqlLite/create.py diff --git a/sqlLite/get.py b/OLD/sqlLite/get.py similarity index 100% rename from sqlLite/get.py rename to OLD/sqlLite/get.py diff --git a/sqlLite/set.py b/OLD/sqlLite/set.py similarity index 100% rename from sqlLite/set.py rename to OLD/sqlLite/set.py diff --git a/standard/getter.py b/OLD/standard/getter.py similarity index 100% rename from standard/getter.py rename to OLD/standard/getter.py diff --git a/config/database.py b/config/database.py new file mode 100644 index 0000000..1a311fb --- /dev/null +++ b/config/database.py @@ -0,0 +1,46 @@ +import os +from typing import Dict, Any + + +class DatabaseConfig: + def __init__(self): + # Environment-basierte Konfiguration + self.db_type = os.getenv('DB_TYPE', 'sqlite') + self.config = self._load_config() + + def _load_config(self) -> Dict[str, Any]: + if self.db_type.lower() == 'sqlite': + return { + 'path': os.getenv('SQLITE_PATH', 'databases/chatbot.db') + } + elif self.db_type.lower() == 'mysql': + return { + 'host': os.getenv('MYSQL_HOST', 'localhost'), + 'port': int(os.getenv('MYSQL_PORT', '3306')), + 'database': os.getenv('MYSQL_DATABASE', 'chatbot'), + 'user': os.getenv('MYSQL_USER', 'root'), + 'password': os.getenv('MYSQL_PASSWORD', ''), + 'charset': 'utf8mb4' + } + elif self.db_type.lower() == 'mariadb': + return { + 'host': os.getenv('MARIADB_HOST', 'localhost'), + 'port': int(os.getenv('MARIADB_PORT', '3306')), + 'database': os.getenv('MARIADB_DATABASE', 'chatbot'), + 'user': os.getenv('MARIADB_USER', 'root'), + 'password': os.getenv('MARIADB_PASSWORD', ''), + 'charset': 'utf8' + } + elif self.db_type.lower() == 'postgresql': + return { + 'host': os.getenv('POSTGRES_HOST', 'localhost'), + 'port': int(os.getenv('POSTGRES_PORT', '5432')), + 'database': os.getenv('POSTGRES_DATABASE', 'chatbot'), + 'user': os.getenv('POSTGRES_USER', 'postgres'), + 'password': os.getenv('POSTGRES_PASSWORD', ''), + } + else: + raise ValueError(f"Unsupported database type: {self.db_type}") + + def get_database_config(self) -> tuple[str, Dict[str, Any]]: + return self.db_type, self.config \ No newline at end of file diff --git a/database/__init__.py b/database/__init__.py new file mode 100644 index 0000000..8bcd0e4 --- /dev/null +++ b/database/__init__.py @@ -0,0 +1,40 @@ +import logging +from typing import Dict, Any +from .interface import DatabaseInterface +from .sqlite_db import SQLiteDatabase + +logger = logging.getLogger(__name__) + + +class DatabaseFactory: + + @staticmethod + def create_database(db_type: str, config: Dict[str, Any]) -> DatabaseInterface: + + if db_type.lower() == 'sqlite': + if 'path' not in config: + raise ValueError("SQLite configuration requires 'path'") + return SQLiteDatabase(config['path']) + + elif db_type.lower() == 'mysql': + # Für später: MySQL-Implementierung + # from .mysql_db import MySQLDatabase + # return MySQLDatabase(config) + raise NotImplementedError("MySQL support not yet implemented") + + elif db_type.lower() == 'postgresql': + # Für später: PostgreSQL-Implementierung + # from .postgresql_db import PostgreSQLDatabase + # return PostgreSQLDatabase(config) + raise NotImplementedError("PostgreSQL support not yet implemented") + + else: + raise ValueError(f"Unsupported database type: {db_type}") + + +# Convenience-Funktion für einfachen Zugriff +def get_database(db_type: str, config: Dict[str, Any]) -> DatabaseInterface: + db = DatabaseFactory.create_database(db_type, config) + db.connect() + db.create_user_table() + return db diff --git a/database/flask_integration.py b/database/flask_integration.py new file mode 100644 index 0000000..5ecdf25 --- /dev/null +++ b/database/flask_integration.py @@ -0,0 +1,38 @@ +import logging +from flask import g, current_app +from .interface import DatabaseInterface + +logger = logging.getLogger(__name__) + + +class FlaskDatabaseManager: + """Flask-Integration für Thread-Safe Database Management""" + + def __init__(self, database_factory_func): + self.database_factory_func = database_factory_func + + def get_db(self) -> DatabaseInterface: + """ + Holt die Datenbank-Instanz für den aktuellen Request + Verwendet Flask's 'g' object für request-lokale Speicherung + """ + if 'database' not in g: + g.database = self.database_factory_func() + g.database.connect() + g.database.create_user_table() + logger.debug("Database instance created for request") + + return g.database + + def close_db(self, error=None): + """ + Schließt die Datenbank-Verbindung am Ende des Requests + """ + database = g.pop('database', None) + if database is not None: + database.disconnect() + logger.debug("Database connection closed for request") + + def init_app(self, app): + """Registriert die Database-Manager-Funktionen bei Flask""" + app.teardown_appcontext(self.close_db) \ No newline at end of file diff --git a/database/interface.py b/database/interface.py new file mode 100644 index 0000000..18889b1 --- /dev/null +++ b/database/interface.py @@ -0,0 +1,40 @@ +from abc import ABC, abstractmethod +from typing import Optional, List, Dict, Any + + +class DatabaseInterface(ABC): + @abstractmethod + def connect(self) -> None: + pass + + @abstractmethod + def disconnect(self) -> None: + pass + + @abstractmethod + def create_user_table(self) -> None: + pass + + @abstractmethod + def create_user(self, username: str, email: str, password_hash: str) -> bool: + pass + + @abstractmethod + def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]: + pass + + @abstractmethod + def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]: + pass + + @abstractmethod + def get_password_hash_by_email(self, email: str) -> Optional[str]: + pass + + @abstractmethod + def update_user_password(self, email: str, new_password_hash: str) -> bool: + pass + + @abstractmethod + def delete_user(self, email: str) -> bool: + pass diff --git a/database/mysql_db.py b/database/mysql_db.py new file mode 100644 index 0000000..e69de29 diff --git a/database/postgresql_db.py b/database/postgresql_db.py new file mode 100644 index 0000000..e69de29 diff --git a/database/sqlite_db.py b/database/sqlite_db.py new file mode 100644 index 0000000..a07831d --- /dev/null +++ b/database/sqlite_db.py @@ -0,0 +1,189 @@ +import sqlite3 +import logging +from threading import local +from typing import Optional, Dict, Any +from .interface import DatabaseInterface + +logger = logging.getLogger(__name__) + + +class SQLiteDatabase(DatabaseInterface): + """Thread-Safe SQLite-Implementierung der Database-Interface""" + + def __init__(self, db_path: str): + self.db_path = db_path + # Thread-local storage für Verbindungen + self._local = local() + + def _get_connection(self) -> sqlite3.Connection: + """Holt oder erstellt eine thread-lokale Verbindung""" + if not hasattr(self._local, 'connection') or self._local.connection is None: + self._local.connection = sqlite3.connect( + self.db_path, + check_same_thread=False, # Erlaubt thread-übergreifende Nutzung + timeout=30.0, + detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES + ) + self._local.connection.row_factory = sqlite3.Row + # Optimierungen für bessere Performance + self._local.connection.execute("PRAGMA journal_mode=WAL") + self._local.connection.execute("PRAGMA synchronous=NORMAL") + self._local.connection.execute("PRAGMA cache_size=1000") + self._local.connection.execute("PRAGMA temp_store=MEMORY") + logger.debug(f"New SQLite connection created for thread") + + return self._local.connection + + def connect(self) -> None: + """Initialisiert die thread-lokale Verbindung""" + try: + conn = self._get_connection() + logger.info(f"Connected to SQLite database: {self.db_path}") + except Exception as e: + logger.error(f"Failed to connect to SQLite database: {e}") + raise + + def disconnect(self) -> None: + """Schließt die thread-lokale Verbindung""" + if hasattr(self._local, 'connection') and self._local.connection: + try: + self._local.connection.close() + self._local.connection = None + logger.debug("SQLite connection closed for thread") + except Exception as e: + logger.warning(f"Error closing SQLite connection: {e}") + + def create_user_table(self) -> None: + """User-Tabelle erstellen""" + conn = self._get_connection() + cursor = conn.cursor() + try: + cursor.execute(""" + CREATE TABLE IF NOT EXISTS users + ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) + """) + conn.commit() + logger.info("User table created/verified") + except Exception as e: + logger.error(f"Failed to create user table: {e}") + raise + finally: + cursor.close() + + def create_user(self, username: str, email: str, password_hash: str) -> bool: + """Neuen User erstellen""" + conn = self._get_connection() + cursor = conn.cursor() + try: + cursor.execute( + "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)", + (username, email, password_hash) + ) + conn.commit() + logger.info(f"User created successfully: {email}") + return True + except sqlite3.IntegrityError as e: + logger.warning(f"User creation failed (duplicate): {e}") + conn.rollback() + return False + except Exception as e: + logger.error(f"Failed to create user: {e}") + conn.rollback() + raise + finally: + cursor.close() + + def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]: + """User anhand Email suchen""" + conn = self._get_connection() + cursor = conn.cursor() + try: + cursor.execute( + "SELECT id, username, email, password_hash, created_at FROM users WHERE email = ?", + (email,) + ) + row = cursor.fetchone() + return dict(row) if row else None + except Exception as e: + logger.error(f"Failed to get user by email: {e}") + raise + finally: + cursor.close() + + def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]: + """User anhand Username suchen""" + conn = self._get_connection() + cursor = conn.cursor() + try: + cursor.execute( + "SELECT id, username, email, password_hash, created_at FROM users WHERE username = ?", + (username,) + ) + row = cursor.fetchone() + return dict(row) if row else None + except Exception as e: + logger.error(f"Failed to get user by username: {e}") + raise + finally: + cursor.close() + + def get_password_hash_by_email(self, email: str) -> Optional[str]: + """Password-Hash für Email abrufen""" + conn = self._get_connection() + cursor = conn.cursor() + try: + cursor.execute( + "SELECT password_hash FROM users WHERE email = ?", + (email,) + ) + row = cursor.fetchone() + return row if row else None + except Exception as e: + logger.error(f"Failed to get password hash by email: {e}") + raise + finally: + cursor.close() + + def update_user_password(self, email: str, new_password_hash: str) -> bool: + """Passwort aktualisieren""" + conn = self._get_connection() + cursor = conn.cursor() + try: + cursor.execute( + "UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE email = ?", + (new_password_hash, email) + ) + conn.commit() + success = cursor.rowcount > 0 + if success: + logger.info(f"Password updated for user: {email}") + return success + except Exception as e: + logger.error(f"Failed to update password: {e}") + conn.rollback() + raise + finally: + cursor.close() + + def delete_user(self, email: str) -> bool: + """User löschen""" + conn = self._get_connection() + cursor = conn.cursor() + try: + cursor.execute("DELETE FROM users WHERE email = ?", (email,)) + conn.commit() + success = cursor.rowcount > 0 + if success: + logger.info(f"User deleted: {email}") + return success + except Exception as e: + logger.error(f"Failed to delete user: {e}") + conn.rollback() + raise + finally: + cursor.close() \ No newline at end of file diff --git a/frontend/app.py b/frontend/app.py index 31a73c2..dbf98f9 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -1,79 +1,273 @@ -from flask import Flask, render_template, request, redirect, url_for, flash +from datetime import datetime, timedelta # Add this line at the top +from flask import Flask, render_template, request, redirect, url_for, flash, session +import logging +from config.database import DatabaseConfig +from database import DatabaseFactory +from database.flask_integration import FlaskDatabaseManager +from services.user_service import UserService +from services.auth_service import AuthService +from utils.auth_decorators import login_required, logout_required, get_current_user -import hashlib -def hash_password(password): - return hashlib.sha512(password.strip().encode('utf-8')).hexdigest() +# Logging konfigurieren +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) app = Flask(__name__) -app.secret_key = 'your_secret_key' +app.secret_key = 'your-secret-key-change-this-in-production' + +# Session-Konfiguration +app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=24) # Session läuft nach 24h ab +app.config['SESSION_COOKIE_SECURE'] = False # Für Development - in Production auf True setzen +app.config['SESSION_COOKIE_HTTPONLY'] = True # Verhindert XSS-Angriffe + + +# Database Manager initialisieren +db_config = DatabaseConfig() +db_type, config = db_config.get_database_config() + + +def create_database(): + """Factory-Funktion für Datenbank-Instanzen""" + return DatabaseFactory.create_database(db_type, config) + + +db_manager = FlaskDatabaseManager(create_database) +db_manager.init_app(app) + + +# Template-Context für alle Templates +@app.context_processor +def inject_user(): + """Macht User-Daten in allen Templates verfügbar""" + return {'current_user': get_current_user()} + @app.route('/') def home(): + """Startseite - Weiterleitung je nach Login-Status""" + if 'user_id' in session: + return redirect(url_for('dashboard')) return redirect(url_for('login')) + @app.route('/register', methods=['GET', 'POST']) +@logout_required def register(): - import standard.getter as st_getter - import sqlLite.set as setter - + """Registrierung - nur für nicht eingeloggte User""" if request.method == 'POST': - username = request.form['username'] - email = request.form.get('email') - password = request.form.get('password') - pwd_confirm = request.form.get('confirm_password') + username = request.form.get('username', '').strip() + email = request.form.get('email', '').strip() + password = request.form.get('password', '') + confirm_password = request.form.get('confirm_password', '') - if not email or not password or not pwd_confirm or not username: + # Basis-Validierung + if not username or not email or not password or not confirm_password: flash('Please fill out all fields', 'error') return redirect(url_for('register')) - if password != pwd_confirm: + if password != confirm_password: flash('Passwords do not match', 'error') return redirect(url_for('register')) - try: - if st_getter.get_validate_email(email): - setter.set_login(username, email, password) - flash('Registration successful! Please log in.', 'success') - return redirect(url_for('login')) - else: - flash('Invalid email format', 'error') - return redirect(url_for('register')) - except Exception as e: - flash(f'Error: {str(e)}', 'error') + # Services mit request-lokaler DB-Instanz + database = db_manager.get_db() + user_service = UserService(database) + + # User erstellen + success, errors = user_service.create_user(username, email, password) + + if success: + flash('Registration successful! Please log in.', 'success') + logger.info(f"New user registered: {email}") + return redirect(url_for('login')) + else: + for error in errors: + flash(error, 'error') return redirect(url_for('register')) - # For GET-requests: + return render_template('register.html') @app.route('/login', methods=['GET', 'POST']) +@logout_required def login(): + """Login - nur für nicht eingeloggte User""" if request.method == 'POST': - enter_email = request.form.get('email') - enter_password = request.form.get('password') - import hashlib - import sqlLite.get as getter - import standard.getter as st_getter + email = request.form.get('email', '').strip() + password = request.form.get('password', '') + remember_me = request.form.get('remember_me') # Checkbox für "Remember Me" - stored_hash = getter.get_password_by_email(enter_email) # use email here + if not email or not password: + flash('Please enter email and password', 'error') + return redirect(url_for('login')) - if stored_hash is None: - flash("User not found!") - return redirect(url_for("login")) + # Services mit request-lokaler DB-Instanz + database = db_manager.get_db() + auth_service = AuthService(database) - hash_entered = st_getter.get_password_hash(enter_password) + # Authentifizierung + success, user_data, message = auth_service.authenticate(email, password) + + if success: + # Session setzen + session['user_id'] = user_data['id'] + session['username'] = user_data['username'] + session['email'] = user_data['email'] + session['login_time'] = datetime.utcnow().isoformat() + + # Permanent session wenn "Remember Me" aktiviert + if remember_me: + session.permanent = True + + flash(f'Welcome back, {user_data["username"]}!', 'success') + logger.info(f"User logged in: {email}") + + # Weiterleitung zu ursprünglich angeforderte Seite (falls vorhanden) + next_page = request.args.get('next') + if next_page: + return redirect(next_page) - if hash_entered == stored_hash: return redirect(url_for('dashboard')) else: - flash('Invalid email or password', 'error') - print("Stored hash:", stored_hash) - print("Entered hash:", hash_entered) + flash(message, 'error') return redirect(url_for('login')) return render_template('login.html') + +@app.route('/logout') +@login_required +def logout(): + """Logout - nur für eingeloggte User""" + user_email = session.get('email') + username = session.get('username') + + # Session komplett löschen + session.clear() + + flash(f'Goodbye, {username}! You have been logged out successfully.', 'info') + + if user_email: + logger.info(f"User logged out: {user_email}") + + return redirect(url_for('login')) + + @app.route('/dashboard') +@login_required def dashboard(): - #return "Welcome to the dashboard! Login successful." - return render_template('dashboard.html') + """Dashboard - nur für eingeloggte User""" + user = get_current_user() + + # Zusätzliche Dashboard-Daten (optional) + dashboard_data = { + 'total_users': 'N/A', # Könnte aus DB geholt werden + 'last_login': session.get('login_time', 'Unknown'), + 'session_expires': 'Never' if session.permanent else '24 hours' + } + + logger.debug(f"Dashboard accessed by user: {user['email']}") + + return render_template('dashboard.html', + user=user, + dashboard_data=dashboard_data) + + +@app.route('/profile') +@login_required +def profile(): + """User Profile - nur für eingeloggte User""" + user = get_current_user() + + # Hier könnten zusätzliche User-Daten aus der DB geholt werden + database = db_manager.get_db() + full_user_data = database.get_user_by_email(user['email']) + + return render_template('profile.html', user=full_user_data) + + +@app.route('/change-password', methods=['GET', 'POST']) +@login_required +def change_password(): + """Passwort ändern - nur für eingeloggte User""" + if request.method == 'POST': + current_password = request.form.get('current_password', '') + new_password = request.form.get('new_password', '') + confirm_password = request.form.get('confirm_password', '') + + if not current_password or not new_password or not confirm_password: + flash('Please fill out all fields', 'error') + return redirect(url_for('change_password')) + + if new_password != confirm_password: + flash('New passwords do not match', 'error') + return redirect(url_for('change_password')) + + # Aktuelles Passwort verifizieren + user = get_current_user() + database = db_manager.get_db() + auth_service = AuthService(database) + + success, _, message = auth_service.authenticate(user['email'], current_password) + + if not success: + flash('Current password is incorrect', 'error') + return redirect(url_for('change_password')) + + # Neues Passwort setzen + from utils.password_utils import PasswordUtils + new_password_hash = PasswordUtils.hash_password_simple(new_password) + + if database.update_user_password(user['email'], new_password_hash): + flash('Password changed successfully', 'success') + logger.info(f"Password changed for user: {user['email']}") + return redirect(url_for('dashboard')) + else: + flash('Failed to change password', 'error') + return redirect(url_for('change_password')) + + return render_template('change_password.html') + + +# Error Handlers +@app.errorhandler(404) +def not_found(error): + return render_template('404.html'), 404 + + +@app.errorhandler(500) +def internal_error(error): + logger.error(f"Internal server error: {error}") + flash('An internal error occurred. Please try again.', 'error') + return redirect(url_for('home')) + + +# Session Timeout Check +@app.before_request +def check_session_timeout(): + """Prüft ob Session abgelaufen ist""" + from datetime import datetime + + if 'user_id' in session: + # Prüfe ob Session zu alt ist (optional) + login_time = session.get('login_time') + if login_time: + try: + login_datetime = datetime.fromisoformat(login_time) + now = datetime.utcnow() + + # Session nach 24h abgelaufen (falls nicht permanent) + if not session.permanent and (now - login_datetime).total_seconds() > 86400: + session.clear() + flash('Your session has expired. Please log in again.', 'warning') + return redirect(url_for('login')) + except (ValueError, TypeError): + # Ungültiger Zeitstempel - Session löschen + session.clear() + flash('Invalid session. Please log in again.', 'warning') + return redirect(url_for('login')) + + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=8080, threaded=True) \ No newline at end of file diff --git a/frontend/templates/dashboard.html b/frontend/templates/dashboard.html index 5244e39..00969c2 100644 --- a/frontend/templates/dashboard.html +++ b/frontend/templates/dashboard.html @@ -1,119 +1,312 @@ - - - Register - + .btn-primary { + background: #667eea; + color: white; + } + + .btn-primary:hover { + background: #5a67d8; + transform: translateY(-1px); + } + + .btn-secondary { + background: #e2e8f0; + color: #4a5568; + } + + .btn-secondary:hover { + background: #cbd5e0; + } + + .btn-danger { + background: #e53e3e; + color: white; + } + + .btn-danger:hover { + background: #c53030; + transform: translateY(-1px); + } + + .dashboard-content { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 30px; + margin-bottom: 30px; + } + + .card { + background: white; + padding: 25px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); + transition: transform 0.3s ease; + } + + .card:hover { + transform: translateY(-5px); + } + + .card h3 { + color: #333; + font-size: 20px; + font-weight: 600; + margin-bottom: 15px; + } + + .card p { + color: #666; + line-height: 1.6; + margin-bottom: 15px; + } + + .stats { + display: flex; + justify-content: space-between; + margin: 15px 0; + } + + .stat-item { + text-align: center; + } + + .stat-value { + font-size: 24px; + font-weight: 700; + color: #667eea; + } + + .stat-label { + font-size: 12px; + color: #666; + text-transform: uppercase; + letter-spacing: 1px; + } + + .alert { + padding: 15px 20px; + margin: 20px 0; + border-radius: 10px; + font-weight: 500; + } + + .alert-success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + } + + .alert-error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + } + + .alert-warning { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; + } + + .alert-info { + background: #cce7ff; + color: #004085; + border: 1px solid #b8daff; + } + + .quick-actions { + background: white; + padding: 25px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); + grid-column: 1 / -1; + } + + .quick-actions h3 { + margin-bottom: 20px; + color: #333; + } + + .action-buttons { + display: flex; + gap: 15px; + flex-wrap: wrap; + } + + @media (max-width: 768px) { + .header { + flex-direction: column; + gap: 20px; + text-align: center; + } + + .user-actions { + flex-direction: column; + width: 100%; + } + + .dashboard-content { + grid-template-columns: 1fr; + } + + .action-buttons { + flex-direction: column; + } + } + +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + + +
+
+

Welcome, {{ user.username }}! 👋

+

Here's your ChatBot dashboard overview

+
+ +
+ + +
+
+

Account Information

+

Username: {{ user.username }}

+

Email: {{ user.email }}

+

User ID: #{{ user.id }}

+

Last Login: {{ dashboard_data.last_login[:19] if dashboard_data.last_login != 'Unknown' else 'Unknown' }}

+ Change Password +
+ +
+

Session Information

+

Session Status: Active

+

Session Type: {{ 'Persistent' if session.permanent else 'Temporary' }}

+

Expires: {{ dashboard_data.session_expires }}

+
+
+
{{ user.id }}
+
User ID
+
+
+
+ +
+

ChatBot Status

+

Your personal chatbot is ready to help you.

+

Status: Active

+

Total Users: {{ dashboard_data.total_users }}

+ Start Chat +
+ + + +
+
- + \ No newline at end of file diff --git a/frontend/templates/login.html b/frontend/templates/login.html index 82d8a0c..8fee8bb 100644 --- a/frontend/templates/login.html +++ b/frontend/templates/login.html @@ -1,141 +1,190 @@ - - - Login - + .checkbox-group label { + margin: 0; + font-size: 14px; + color: #666; + } + + button { + width: 100%; + padding: 16px 0; + margin-top: 10px; + background-color: #2575fc; + border: none; + border-radius: 14px; + color: white; + font-size: 18px; + font-weight: 700; + letter-spacing: 1px; + cursor: pointer; + box-shadow: 0 10px 20px rgba(37, 117, 252, 0.4); + transition: background-color 0.3s ease, box-shadow 0.3s ease; + } + + button:hover { + background-color: #1859d6; + box-shadow: 0 15px 25px rgba(24, 89, 214, 0.6); + } + + .register-link { + margin-top: 20px; + font-size: 14px; + color: #666; + } + + .register-link a { + color: #2575fc; + text-decoration: none; + font-weight: 600; + } + + .register-link a:hover { + text-decoration: underline; + } + + .error-message { + margin-top: 20px; + color: #ff4d4f; + font-weight: 600; + font-size: 15px; + text-align: center; + background: #ffe6e6; + padding: 10px 15px; + border-radius: 10px; + box-shadow: 0 2px 6px rgba(255,77,79,0.3); + display: none; + } + -
-

Login to Your Account

- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
- {% for category, message in messages %} - {{ message }} - {% endfor %} -
- {% endif %} - {% endwith %} -
-
- - -
-
- - -
- -
-
+
+

Login to Your Account

+ + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} + {% endwith %} + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
- + \ No newline at end of file diff --git a/frontend/templates/profile.html b/frontend/templates/profile.html new file mode 100644 index 0000000..9d1aa2b --- /dev/null +++ b/frontend/templates/profile.html @@ -0,0 +1,155 @@ + + + + + + Profile - ChatBot + + + +
+
+

{{ user.username }}

+

User Profile

+
+ +
+ + +
+
+

Account Details

+

User ID: #{{ user.id }}

+

Username: {{ user.username }}

+

Email: {{ user.email }}

+
+ +
+

Account Status

+

Status: Active

+

Created: {{ user.created_at[:19] if user.created_at else 'Unknown' }}

+

Last Updated: {{ user.updated_at[:19] if user.updated_at else 'Unknown' }}

+
+
+ +
+ Change Password + Back to Dashboard +
+
+
+ + \ No newline at end of file diff --git a/main.py b/main.py index 430e84f..c047fa1 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,28 @@ -import sqlLite - +import logging +import os from frontend.app import app -sqlLite.set_db_name("databases/test.db") +# Logging konfigurieren +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) -import sqlLite.create as create - -create.create_table_t_user() +logger = logging.getLogger(__name__) if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=8080) + # Environment-Konfiguration + debug = os.getenv('FLASK_DEBUG', 'False').lower() == 'true' + host = os.getenv('FLASK_HOST', '0.0.0.0') + port = int(os.getenv('FLASK_PORT', '8080')) + + logger.info(f"Starting ChatBot application on {host}:{port}") + logger.info(f"Debug mode: {debug}") + + try: + app.run(debug=debug, host=host, port=port) + except KeyboardInterrupt: + logger.info("Application stopped by user") + except Exception as e: + logger.error(f"Application failed to start: {e}") + raise \ No newline at end of file diff --git a/models/user.py b/models/user.py new file mode 100644 index 0000000..e69de29 diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/auth_service.py b/services/auth_service.py new file mode 100644 index 0000000..ec8141e --- /dev/null +++ b/services/auth_service.py @@ -0,0 +1,50 @@ +import logging +from typing import Optional, Dict, Any +from database.interface import DatabaseInterface +from utils.password_utils import PasswordUtils +from utils.validation import ValidationUtils + +logger = logging.getLogger(__name__) + +class AuthService: + + def __init__(self, database: DatabaseInterface): + self.db = database + + def authenticate(self, email: str, password: str) -> tuple[bool, Optional[Dict[str, Any]], str]: + if not ValidationUtils.validate_email(email): + return False, None, "Invalid email format" + + if not password: + return False, None, "Password is required" + try: + # User holen + user = self.db.get_user_by_email(email.lower()) + if not user: + logger.warning(f"Authentication failed: user not found for email {email}") + return False, None, "Invalid email or password" + # Passwort prüfen + stored_hash = user.get('password_hash') + if not stored_hash: + logger.error(f"No password hash found for user {email}") + return False, None, "Authentication error" + # Einfacher Hash-Vergleich (für Rückwärtskompatibilität) + entered_hash = PasswordUtils.hash_password_simple(password) + + if entered_hash == stored_hash: + logger.info(f"Authentication successful for user: {email}") + # Sensible Daten nicht zurückgeben + safe_user_data = { + 'id': user['id'], + 'username': user['username'], + 'email': user['email'], + 'created_at': user.get('created_at') + } + return True, safe_user_data, "Authentication successful" + else: + logger.warning(f"Authentication failed: wrong password for email {email}") + return False, None, "Invalid email or password" + + except Exception as e: + logger.error(f"Authentication error for email {email}: {e}") + return False, None, "Authentication error" diff --git a/services/user_service.py b/services/user_service.py new file mode 100644 index 0000000..e12dd09 --- /dev/null +++ b/services/user_service.py @@ -0,0 +1,73 @@ +import logging +from typing import Optional, Dict, Any +from database.interface import DatabaseInterface +from utils.password_utils import PasswordUtils +from utils.validation import ValidationUtils + +logger = logging.getLogger(__name__) + +class UserService: + def __init__(self, database: DatabaseInterface): + self.db = database + + def create_user(self, username: str, email: str, password: str) -> tuple[bool, list[str]]: + errors = [] + + # Validierung + if not ValidationUtils.validate_username(username): + errors.append("Invalid username format") + + if not ValidationUtils.validate_email(email): + errors.append("Invalid email format") + + password_valid, password_errors = ValidationUtils.validate_password(password) + if not password_valid: + errors.extend(password_errors) + + if errors: + return False, errors + + # Prüfe auf existierende User + if self.get_user_by_email(email): + errors.append("Email already registered") + + if self.get_user_by_username(username): + errors.append("Username already taken") + + if errors: + return False, errors + + # Passwort hashen + password_hash = PasswordUtils.hash_password_simple(password) + + # User erstellen + try: + success = self.db.create_user(username, email.lower(), password_hash) + if success: + logger.info(f"User created successfully: {email}") + return True, [] + else: + logger.warning(f"Failed to create user: {email}") + return False, ["Failed to create user"] + except Exception as e: + logger.error(f"Error creating user: {e}") + return False, [f"Database error: {str(e)}"] + + + def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]: + if not ValidationUtils.validate_email(email): + return None + try: + return self.db.get_user_by_email(email.lower()) + except Exception as e: + logger.error(f"Error getting user by email: {e}") + return None + + def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]: + if not ValidationUtils.validate_username(username): + return None + try: + return self.db.get_user_by_username(username) + except Exception as e: + logger.error(f"Error getting user by username: {e}") + return None \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/auth_decorators.py b/utils/auth_decorators.py new file mode 100644 index 0000000..bc6001b --- /dev/null +++ b/utils/auth_decorators.py @@ -0,0 +1,43 @@ +from functools import wraps +from flask import session, redirect, url_for, flash, request +import logging + +logger = logging.getLogger(__name__) + +def login_required(f): + """ + Decorator to protect routes that require authentication + """ + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session or 'email' not in session: + flash('Please log in to access this page', 'warning') + logger.info(f"Unauthorized access attempt to {request.endpoint}") + return redirect(url_for('login')) + return f(*args, **kwargs) + return decorated_function + +def logout_required(f): + """ + Decorator for routes that should only be accessible when NOT logged in + (e.g., login, register pages) + """ + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' in session: + flash('You are already logged in', 'info') + return redirect(url_for('dashboard')) + return f(*args, **kwargs) + return decorated_function + +def get_current_user(): + """ + Helper function to get current user info from session + """ + if 'user_id' in session: + return { + 'id': session['user_id'], + 'username': session['username'], + 'email': session['email'] + } + return None \ No newline at end of file diff --git a/utils/password_utils.py b/utils/password_utils.py new file mode 100644 index 0000000..b08be81 --- /dev/null +++ b/utils/password_utils.py @@ -0,0 +1,27 @@ +import hashlib +import secrets +import logging + +logger = logging.getLogger(__name__) + +class PasswordUtils: + @staticmethod + def hash_password(password: str, salt: str = None) -> tuple[str, str]: + if salt is None: + salt = secrets.token_hex(32) + password = password.strip() + salted_password = password + salt + hash_object = hashlib.sha512(salted_password.encode('utf-8')) + password_hash = hash_object.hexdigest() + logger.debug("Password hashed successfully") + return password_hash, salt + + @staticmethod + def verify_password(password: str, stored_hash: str, salt: str) -> bool: + computed_hash, _ = PasswordUtils.hash_password(password, salt) + return secrets.compare_digest(computed_hash, stored_hash) + + @staticmethod + def hash_password_simple(password: str) -> str: + password = password.strip() + return hashlib.sha512(password.encode('utf-8')).hexdigest() \ No newline at end of file diff --git a/utils/validation.py b/utils/validation.py new file mode 100644 index 0000000..eee1154 --- /dev/null +++ b/utils/validation.py @@ -0,0 +1,56 @@ +import re +import logging + +logger = logging.getLogger(__name__) + +class ValidationUtils: + @staticmethod + def validate_email(email: str) -> bool: + if not email or not isinstance(email, str): + return False + + email = email.strip().lower() + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + is_valid = bool(re.match(pattern, email)) + + if not is_valid: + logger.warning(f"Invalid email format: {email}") + + return is_valid + + @staticmethod + def validate_username(username: str) -> bool: + if not username or not isinstance(username, str): + return False + username = username.strip() + # Username-Regeln: 3-25 Zeichen, nur Buchstaben, Zahlen und Unterstrich + if len(username) < 3 or len(username) > 25: + logger.warning(f"Username length invalid: {len(username)}") + return False + pattern = r'^[a-zA-Z0-9_]+$' + is_valid = bool(re.match(pattern, username)) + if not is_valid: + logger.warning(f"Invalid username format: {username}") + return is_valid + + @staticmethod + def validate_password(password: str) -> tuple[bool, list[str]]: + errors = [] + if not password or not isinstance(password, str): + errors.append("Password is required") + return False, errors + if len(password) < 4 or len(password) > 50: + errors.append("Password must be at least 4 characters long and must not exceed 128 characters") + if not re.search(r'[A-Z]', password): + errors.append("Password must contain at least one uppercase letter") + if not re.search(r'[a-z]', password): + errors.append("Password must contain at least one lowercase letter") + if not re.search(r'\d', password): + errors.append("Password must contain at least one digit") + if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password): + errors.append("Password must contain at least one special character") + is_valid = len(errors) == 0 + if not is_valid: + logger.warning(f"Password validation failed: {errors}") + + return is_valid, errors \ No newline at end of file