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 @@
- - -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 Status: Active
+Session Type: {{ 'Persistent' if session.permanent else 'Temporary' }}
+Expires: {{ dashboard_data.session_expires }}
+Your personal chatbot is ready to help you.
+Status: Active
+Total Users: {{ dashboard_data.total_users }}
+ Start Chat +