import sqlite3 from flask import Flask, render_template, request, redirect, url_for, session, g, flash, jsonify from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash import smtplib, ssl, secrets from datetime import datetime, timedelta import json SMTP_SERVER = "smtp.gmail.com" SMTP_PORT = 465 EMAIL_SENDER = "clickcandit@gmail.com" EMAIL_PASSWORD = "iuxexntistlwilhl" app = Flask(__name__) app.secret_key = "SUPER_SECRET_KEY" # Bitte anpassen DATABASE = 'clickcandit.db' def get_db(): db = getattr(g, '_database', None) if db is None: db = g._database = sqlite3.connect(DATABASE) db.row_factory = sqlite3.Row return db @app.teardown_appcontext def close_connection(exception): db = getattr(g, '_database', None) if db is not None: db.close() def init_db(): with app.app_context(): db = get_db() db.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, email TEXT, is_admin INTEGER DEFAULT 0 ); """) db.execute(""" CREATE TABLE IF NOT EXISTS bookmarks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, title TEXT NOT NULL, url TEXT NOT NULL, icon_class TEXT NOT NULL DEFAULT 'fas fa-bookmark', FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); """) db.execute(""" CREATE TABLE IF NOT EXISTS notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, message TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, is_read INTEGER DEFAULT 0 ); """) db.execute(""" CREATE TABLE IF NOT EXISTS reset_tokens ( user_id INTEGER NOT NULL, token TEXT NOT NULL, expires_at DATETIME NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); """) db.execute(""" CREATE TABLE IF NOT EXISTS time_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, activity TEXT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); """) db.execute(""" CREATE TABLE IF NOT EXISTS user_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, wallpaper TEXT DEFAULT '1.png', city TEXT DEFAULT '', show_forecast INTEGER DEFAULT 0, bookmarks TEXT DEFAULT '', FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ); """) db.commit() # ------------------------------------------------------------ # HILFSFUNKTIONEN # ------------------------------------------------------------ def get_user_by_username_or_email(user_input): db = get_db() return db.execute(""" SELECT * FROM users WHERE username = :val OR email = :val """, {"val": user_input}).fetchone() def get_user_by_username(username): db = get_db() return db.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone() def get_user_by_id(user_id): db = get_db() return db.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone() def is_admin(): return session.get('is_admin', 0) == 1 # ------------------------------------------------------------ # STARTSEITE # ------------------------------------------------------------ @app.route('/') def index(): if 'user_id' in session: return redirect(url_for('dashboard')) return redirect(url_for('login')) # ------------------------------------------------------------ # REGISTRIEREN: erster User wird Admin # ------------------------------------------------------------ @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form.get('username') email = request.form.get('email') password = request.form.get('password') if not username or not email or not password: flash('Bitte alle Felder ausfüllen!', 'red') return redirect(url_for('register')) db = get_db() # Prüfen, ob Benutzer/E-Mail existieren existing = db.execute("SELECT * FROM users WHERE username=? OR email=?", (username, email)).fetchone() if existing: flash('Benutzername oder E-Mail ist bereits vergeben!', 'red') return redirect(url_for('register')) # Erster registrierter User wird Admin count = db.execute("SELECT COUNT(*) as cnt FROM users").fetchone()['cnt'] is_admin_val = 1 if count == 0 else 0 hashed_pw = generate_password_hash(password) db.execute(""" INSERT INTO users (username, password, email, is_admin) VALUES (?, ?, ?, ?) """, (username, hashed_pw, email, is_admin_val)) db.commit() # OPTIONAL: initialer Eintrag in user_settings if is_admin_val == 1: # Falls du globale Settings pro Benutzer willst user_id = db.execute("SELECT id FROM users WHERE username=?",(username,)).fetchone()['id'] db.execute(""" INSERT INTO user_settings (user_id, wallpaper, city, show_forecast, bookmarks) VALUES (?, '1.png', '', 0, '') """, (user_id,)) db.commit() flash('Registrierung erfolgreich! Bitte melde dich an.', 'green') return redirect(url_for('login')) return render_template('register.html') # ------------------------------------------------------------ # LOGIN / LOGOUT # ------------------------------------------------------------ @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') user = get_user_by_username(username) if user and check_password_hash(user['password'], password): session['user_id'] = user['id'] session['username'] = user['username'] session['is_admin'] = user['is_admin'] flash("Login erfolgreich!", "green") return redirect(url_for('dashboard')) else: flash("Benutzername oder Passwort falsch!", "red") return render_template('login.html') @app.route('/logout', methods=['GET', 'POST']) def logout(): session.clear() flash("Erfolgreich abgemeldet!", "green") return redirect(url_for('login')) # ------------------------------------------------------------ # PASSWORT VERGESSEN # ------------------------------------------------------------ @app.route('/reset_password/', methods=['GET', 'POST']) def reset_password(token): db = get_db() row = db.execute(""" SELECT * FROM reset_tokens WHERE token = ? """, (token,)).fetchone() if not row: flash("Ungültiges oder bereits benutztes Token!", "red") return redirect(url_for('forgot_password')) # Ablaufdatum prüfen expires_at = datetime.fromisoformat(row['expires_at']) if datetime.now() > expires_at: flash("Token abgelaufen! Bitte erneut anfordern.", "red") db.execute("DELETE FROM reset_tokens WHERE token=?", (token,)) db.commit() return redirect(url_for('forgot_password')) if request.method == 'POST': pw1 = request.form.get('pw1') pw2 = request.form.get('pw2') if not pw1 or not pw2: flash("Bitte beide Felder ausfüllen!", "red") return redirect(url_for('reset_password', token=token)) if pw1 != pw2: flash("Passwörter stimmen nicht überein!", "red") return redirect(url_for('reset_password', token=token)) hashed_pw = generate_password_hash(pw1) # Passwort setzen db.execute("UPDATE users SET password=? WHERE id=?", (hashed_pw, row['user_id'])) # Token direkt löschen db.execute("DELETE FROM reset_tokens WHERE token=?", (token,)) db.commit() flash("Passwort erfolgreich zurückgesetzt! Du kannst dich jetzt einloggen.", "green") return redirect(url_for('login')) # GET: Formular return render_template('reset_password.html', token=token) @app.route('/forgot_password', methods=['GET', 'POST']) def forgot_password(): if request.method == 'POST': user_input = request.form.get('username_or_email') if not user_input: flash("Bitte Benutzernamen oder E-Mail eingeben!", "red") return redirect(url_for('forgot_password')) db = get_db() # Schaue nach Username oder Email user = db.execute(""" SELECT * FROM users WHERE username=? OR email=? """, (user_input, user_input)).fetchone() if user: # Token generieren token = secrets.token_urlsafe(32) expires = datetime.now() + timedelta(hours=1) db.execute(""" INSERT INTO reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?) """, (user['id'], token, expires)) db.commit() reset_link = f"{request.url_root}reset_password/{token}" subject = "Passwort zurücksetzen" body = f"""Hallo {user['username']}, klicke auf folgenden Link, um dein Passwort zurückzusetzen (gültig 1 Stunde): {reset_link} Wenn du das nicht angefordert hast, ignoriere diese Nachricht. """ message = f"Subject: {subject}\n\n{body}" context = ssl.create_default_context() try: with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, context=context) as server: server.login(EMAIL_SENDER, EMAIL_PASSWORD) server.sendmail(EMAIL_SENDER, user['email'], message) flash("Eine E-Mail zum Zurücksetzen deines Passworts wurde versendet!", "green") except Exception as e: flash(f"Fehler beim Senden der E-Mail: {e}", "red") return redirect(url_for('login')) else: flash("Kein Nutzer gefunden!", "red") return redirect(url_for('forgot_password')) return render_template('forgot_password.html') # ------------------------------------------------------------ # ADMIN-BEREICH # ------------------------------------------------------------ @app.route('/admin', methods=['GET', 'POST']) def admin_panel(): if not is_admin(): flash("Zugriff verweigert!", "red") return redirect(url_for('dashboard')) db = get_db() if request.method == 'POST': new_username = request.form.get('new_username') new_password = request.form.get('new_password') new_is_admin = 1 if request.form.get('new_is_admin') == 'on' else 0 if new_username and new_password: hashed_pw = generate_password_hash(new_password) try: db.execute("INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)", (new_username, hashed_pw, new_is_admin)) db.commit() flash("Neuer Benutzer erstellt!", "green") except sqlite3.IntegrityError: flash("Benutzername bereits vorhanden!", "red") users = db.execute("SELECT * FROM users").fetchall() return render_template('admin.html', users=users) @app.route('/admin/delete_user/', methods=['POST']) def delete_user(user_id): if not is_admin(): flash("Zugriff verweigert!", "red") return redirect(url_for('dashboard')) if user_id == session.get('user_id'): flash("Du kannst dich nicht selbst löschen!", "red") return redirect(url_for('admin_panel')) db = get_db() db.execute("DELETE FROM users WHERE id=?", (user_id,)) db.commit() flash("Benutzer gelöscht!", "green") return redirect(url_for('admin_panel')) # ------------------------------------------------------------ # BENACHRICHTIGUNGEN # ------------------------------------------------------------ @app.route('/admin/notifications', methods=['POST']) def add_notification(): if not is_admin(): flash("Zugriff verweigert!", "red") return redirect(url_for('dashboard')) message = request.form.get('message') user_id = request.form.get('user_id') if message: db = get_db() if user_id == "all": db.execute("INSERT INTO notifications (user_id, message) VALUES (NULL, ?)", (message,)) else: db.execute("INSERT INTO notifications (user_id, message) VALUES (?, ?)", (user_id, message)) db.commit() flash("Benachrichtigung erstellt!", "green") return redirect(url_for('admin_panel')) # ------------------------------------------------------------ # BOOKMARKS # ------------------------------------------------------------ @app.route('/admin/bookmarks/', methods=['GET', 'POST']) def manage_bookmarks(user_id): if not is_admin(): flash("Zugriff verweigert!", "red") return redirect(url_for('dashboard')) db = get_db() user = get_user_by_id(user_id) if not user: flash("Benutzer nicht gefunden!", "red") return redirect(url_for('admin_panel')) if request.method == 'POST': title = request.form.get('title') url_ = request.form.get('url') icon = request.form.get('icon_class', 'fas fa-bookmark') if title and url_: db.execute("INSERT INTO bookmarks (user_id, title, url, icon_class) VALUES (?, ?, ?, ?)", (user_id, title, url_, icon)) db.commit() flash("Neues Lesezeichen hinzugefügt!", "green") bookmarks = db.execute("SELECT * FROM bookmarks WHERE user_id=?", (user_id,)).fetchall() return render_template('admin.html', single_user=user, bookmarks=bookmarks) @app.route('/admin/delete_bookmark//', methods=['POST']) def delete_bookmark(bookmark_id, user_id): if not is_admin(): flash("Zugriff verweigert!", "red") return redirect(url_for('dashboard')) db = get_db() db.execute("DELETE FROM bookmarks WHERE id=?", (bookmark_id,)) db.commit() flash("Lesezeichen gelöscht!", "green") return redirect(url_for('manage_bookmarks', user_id=user_id)) # ------------------------------------------------------------ # ZEITERFASSUNG # ------------------------------------------------------------ @app.route('/time_tracking', methods=['POST']) def time_tracking(): if 'user_id' not in session: flash("Bitte erst einloggen!", "red") return redirect(url_for('login')) action = request.form.get('action') activity = request.form.get('activity') db = get_db() if action == 'start': if activity: db.execute(""" INSERT INTO time_entries (user_id, activity, start_time) VALUES (?, ?, ?) """, (session['user_id'], activity, datetime.now())) db.commit() flash("Zeiterfassung gestartet!", "green") else: flash("Bitte einen Aktivitätsnamen angeben!", "red") elif action == 'stop': open_entry = db.execute(""" SELECT * FROM time_entries WHERE user_id = ? AND end_time IS NULL ORDER BY start_time DESC LIMIT 1 """, (session['user_id'],)).fetchone() if open_entry: db.execute("UPDATE time_entries SET end_time=? WHERE id=?", (datetime.now(), open_entry['id'])) db.commit() flash("Zeiterfassung gestoppt!", "green") else: flash("Keine laufende Zeiterfassung gefunden!", "red") return redirect(url_for('dashboard')) @app.route('/admin/time_tracking', methods=['GET']) def admin_time_tracking(): if not is_admin(): flash("Zugriff verweigert!", "red") return redirect(url_for('dashboard')) db = get_db() # Hole alle Zeiteinträge (JOIN mit users, damit wir den username sehen) time_entries = db.execute(""" SELECT t.id, t.user_id, t.activity, t.start_time, t.end_time, u.username FROM time_entries t JOIN users u ON t.user_id = u.id ORDER BY t.start_time DESC """).fetchall() return render_template('admin_time_entries.html', time_entries=time_entries) # ------------------------------------------------------------ # DASHBOARD # ------------------------------------------------------------ @app.route('/dashboard') def dashboard(): if 'user_id' not in session: flash("Bitte melde dich an!", "red") return redirect(url_for('login')) user_id = session['user_id'] db = get_db() # User und Settings ermitteln user = db.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone() if not user: flash("Benutzer nicht gefunden.", "red") return redirect(url_for('logout')) settings = db.execute("SELECT * FROM user_settings WHERE user_id = ?", (user_id,)).fetchone() # Standardwerte aus settings if not settings: wallpaper = '19.png' city = 'Berlin' show_forecast = True # (Dieses 'bookmarks' ist optional, kannst du weglassen, # wenn du ausschließlich die DB-Tabelle 'bookmarks' nutzt) bookmarks_setting = [] else: wallpaper = settings['wallpaper'] city = settings['city'] show_forecast = bool(settings['show_forecast']) if settings['bookmarks']: bookmarks_setting = settings['bookmarks'].split(",") # Nur als Fallback else: bookmarks_setting = [] # DB-Bookmarks für den eingeloggten User user_bookmarks = db.execute(""" SELECT id, title, url, icon_class FROM bookmarks WHERE user_id=? ORDER BY id DESC """, (user_id,)).fetchall() # Notifications für diesen User (user_id) oder globale (user_id IS NULL) notifications = db.execute(""" SELECT id, user_id, message, created_at FROM notifications WHERE user_id=? OR user_id IS NULL ORDER BY created_at DESC """, (user_id,)).fetchall() # Ggf. Time-Entries (falls du sie zeigen willst) time_entries = db.execute(""" SELECT * FROM time_entries WHERE user_id=? ORDER BY start_time DESC """, (user_id,)).fetchall() # Beispiel-Apps user_app_chunks = [ [ { 'name': 'Mail', 'subdomain': 'mail', 'appkey': 'mailapp', 'icon_class': 'fa fa-envelope', 'bg_color': 'bg-blue-500' }, { 'name': 'Calendar', 'subdomain': 'calendar', 'appkey': 'calendarapp', 'icon_class': 'fa fa-calendar', 'bg_color': 'bg-green-500' }, ] ] # Wetter (Dummy oder API) current_temp, weather_icon, forecast = get_weather(city) if current_temp is None: current_temp = "N/A" weather_icon = "fa-question" forecast = [] domain = "clickcandit.com/login" logo_path = url_for('static', filename='clickcandit.png') return render_template( 'dashboard.html', user=user['username'], wallpaper=wallpaper, city=city, show_forecast=show_forecast, bookmarks=bookmarks_setting, # falls noch gebraucht, sonst weglassen current_temp=current_temp, weather_icon=weather_icon, forecast=forecast, domain=domain, logo_path=logo_path, user_app_chunks=user_app_chunks, # Hier die neuen Variablen: user_bookmarks=user_bookmarks, notifications=notifications, time_entries=time_entries ) # ------------------------------------------------------------ # SUPPORT & SETTINGS ROUTEN (Aus V1) # ------------------------------------------------------------ @app.route('/delete_notification/', methods=['POST']) def delete_notification(notif_id): if 'user_id' not in session: flash("Bitte melde dich an!", "red") return redirect(url_for('login')) user_id = session['user_id'] db = get_db() # Prüfen, ob die Notification existiert row = db.execute(""" SELECT user_id FROM notifications WHERE id=? """, (notif_id,)).fetchone() if not row: flash("Benachrichtigung existiert nicht.", "red") return redirect(url_for('dashboard')) # Wenn user_id = NULL => globale Notification # Du kannst selbst definieren, ob jeder sie löschen darf oder nur Admin # Hier: Jeder kann globale Notification löschen, wenn er sie sieht. # Oder du sagst: Nur Admin kann globale Notis löschen -> if row['user_id'] is None and not is_admin(): ... if row['user_id'] is None: # Optional: Nur Admin löschen if not is_admin(): flash('Nicht erlaubt', 'red') return redirect(url_for('dashboard')) pass else: # Wenn es eine user-spezifische Notification ist: user_id muss übereinstimmen if row['user_id'] != user_id: flash("Keine Berechtigung, diese Benachrichtigung zu löschen.", "red") return redirect(url_for('dashboard')) # Benachrichtigung löschen db.execute("DELETE FROM notifications WHERE id=?", (notif_id,)) db.commit() flash("Benachrichtigung gelöscht!", "green") return redirect(url_for('dashboard')) @app.route('/send_support_message', methods=['POST']) def send_support_message(): """ Beispiel-Endpunkt, den dein Support-Modal per Fetch aufruft. Erwartet JSON-Daten: { "email": ..., "problemType": ..., "message": ... } """ data = request.get_json() email = data.get('email') problem_type = data.get('problemType') message = data.get('message') if not email or not message: return jsonify({"success": False, "error": "Fehlende Felder"}), 400 # Hier könntest du z.B. eine E-Mail verschicken oder einen Eintrag in der DB anlegen # Demo: wir legen einfach einen Notification-Eintrag an db = get_db() db.execute(""" INSERT INTO notifications (user_id, message) VALUES (NULL, ?) """, (f"Support-Anfrage ({problem_type}) von {email}: {message}",)) db.commit() return jsonify({"success": True}) @app.route('/get_settings', methods=['GET']) def get_settings(): """ Liefert das aktuell gespeicherte Setting zurück (V1-Modal). Du brauchst user_settings oder ein eigenes Modell, wo du die Bookmarks/Stadt/etc. speicherst """ if 'user_id' not in session: return jsonify({"error": "not logged in"}), 403 db = get_db() settings_row = db.execute("SELECT * FROM user_settings WHERE user_id=?", (session['user_id'],)).fetchone() if settings_row: return jsonify({ "wallpaper": settings_row['wallpaper'], "city": settings_row['city'], "show_forecast": bool(settings_row['show_forecast']), "bookmarks": settings_row['bookmarks'].split(",") if settings_row['bookmarks'] else [] }) else: # Falls noch kein Datensatz existiert return jsonify({ "wallpaper": "24.png", "city": "", "show_forecast": False, "bookmarks": [] }) @app.context_processor def inject_wallpaper(): """ Dieser Context Processor wird bei jedem Template-Aufruf ausgeführt. Ermittelt das Wallpaper des eingeloggten Benutzers (falls eingeloggt) und stellt es allen Templates als Variable 'WALLPAPER_URL' bereit. """ if 'user_id' in session: db = get_db() row = db.execute("SELECT wallpaper FROM user_settings WHERE user_id=?", (session['user_id'],)).fetchone() if row and row['wallpaper']: # Bsp: row['wallpaper'] könnte '19.png' o. ä. sein return { "WALLPAPER_URL": url_for('static', filename=row['wallpaper']) } # Fallback: Wenn User nicht eingeloggt oder kein Wallpaper gesetzt return { "WALLPAPER_URL": url_for('static', filename='24.png') } @app.route('/admin/notifications/multi', methods=['POST']) def add_notification_multi(): if not is_admin(): flash("Zugriff verweigert!", "red") return redirect(url_for('dashboard')) message = request.form.get('message') target_list = request.form.getlist('target_users') # ['all'] oder ['1','2'] etc. if not message: flash("Bitte eine Nachricht eingeben.", "red") return redirect(url_for('admin_panel')) db = get_db() # Wenn 'all' in der Liste, Notification für alle if 'all' in target_list: db.execute("INSERT INTO notifications (user_id, message) VALUES (NULL, ?)", (message,)) else: # Sonst für jede ausgewählte ID for uid in target_list: db.execute("INSERT INTO notifications (user_id, message) VALUES (?, ?)", (uid, message)) db.commit() flash("Benachrichtigungen erstellt!", "green") return redirect(url_for('admin_panel')) @app.route('/admin/bookmarks/multi', methods=['POST']) def add_bookmarks_multi(): if not is_admin(): flash("Zugriff verweigert!", "red") return redirect(url_for('dashboard')) # Checkboxen im Template: name='target_users' target_list = request.form.getlist('target_users') # Liste der IDs (Strings) title = request.form.get('title') url_ = request.form.get('url') icon = request.form.get('icon_class', 'fas fa-bookmark') # Ggf. Validierung if not title or not url_: flash("Bitte Titel und URL angeben!", "red") return redirect(url_for('admin_panel')) if not target_list: flash("Bitte mindestens einen Benutzer auswählen!", "red") return redirect(url_for('admin_panel')) icon = request.form.get('icon_class') db = get_db() for uid in target_list: db.execute( "INSERT INTO bookmarks (user_id, title, url, icon_class) VALUES (?, ?, ?, ?)", (uid, title, url_, icon) ) db.commit() flash("Neues Lesezeichen für mehrere Benutzer hinzugefügt!", "green") return redirect(url_for('admin_panel')) @app.route('/save_settings', methods=['POST']) def save_settings(): """ Speichert die Einstellungen, die vom Settings-Modal per Fetch geschickt werden JSON body: { "wallpaper": ..., "city": ..., "show_forecast": ..., "bookmarks": ... } """ if 'user_id' not in session: return jsonify({"success": False, "error": "not logged in"}), 403 data = request.get_json() wallpaper = data.get('wallpaper', '24.png') city = data.get('city', '') show_forecast = data.get('show_forecast', False) bookmarks_list = data.get('bookmarks', []) db = get_db() # Check if row exists existing = db.execute("SELECT id FROM user_settings WHERE user_id=?", (session['user_id'],)).fetchone() if existing: db.execute(""" UPDATE user_settings SET wallpaper=?, city=?, show_forecast=?, bookmarks=? WHERE user_id=? """, (wallpaper, city, int(show_forecast), ",".join(bookmarks_list), session['user_id'])) else: db.execute(""" INSERT INTO user_settings (user_id, wallpaper, city, show_forecast, bookmarks) VALUES (?, ?, ?, ?, ?) """, (session['user_id'], wallpaper, city, int(show_forecast), ",".join(bookmarks_list))) db.commit() return jsonify({"success": True}) def get_weather(city): """ Gibt (current_temp, weather_icon, forecast) zurück """ # Hier kannst du z. B. eine Wetter-API anfragen # oder Dummy-Werte für den Anfang setzen # DUMMY Beispiel: current_temp = 24 weather_icon = "fa-cloud" forecast = [] return (current_temp, weather_icon, forecast) # ------------------------------------------------------------ # STARTUP # ------------------------------------------------------------ if __name__ == '__main__': init_db() app.run(debug=True)