from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify, send_file import sqlite3 import bcrypt import secrets import smtplib import json from datetime import datetime, timedelta import os import requests from email.mime.text import MIMEText app = Flask(__name__, static_folder="static") app.secret_key = 'supersecretkey' DATABASE = "clickcandit.db" SMTP_SERVER = "smtp.gmail.com" SMTP_PORT = 465 EMAIL_SENDER = "clickcandit@gmail.com" EMAIL_PASSWORD = 'iuxexntistlwilhl' WEATHER_API_KEY = '640935f82530489f8c7105323241409' WEATHER_API_URL = 'http://api.openweathermap.org/data/2.5/forecast' EXPORT_FOLDER = "exports" os.makedirs(EXPORT_FOLDER, exist_ok=True) def get_db_connection(): conn = sqlite3.connect(DATABASE) conn.row_factory = sqlite3.Row return conn def init_db(): conn = get_db_connection() c = conn.cursor() # Benutzer (User & Admin) c.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, password TEXT NOT NULL, role TEXT DEFAULT 'user', reset_token TEXT DEFAULT NULL, reset_token_expiry TEXT DEFAULT NULL ) """) # Einstellungen c.execute(""" CREATE TABLE IF NOT EXISTS settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL UNIQUE, wallpaper TEXT DEFAULT '7.png', city TEXT DEFAULT 'Berlin', bookmarks TEXT DEFAULT '[]', show_forecast BOOLEAN DEFAULT 1, FOREIGN KEY(user_id) REFERENCES users(id) ) """) # Download Links (Nutzerdatenexport) c.execute(""" CREATE TABLE IF NOT EXISTS download_links ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, token TEXT NOT NULL, expiration_time TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ) """) conn.commit() conn.close() init_db() ###################################### # Hilfsfunktionen ###################################### def admin_exists(): conn = get_db_connection() result = conn.execute("SELECT 1 FROM users WHERE role = 'admin' LIMIT 1").fetchone() conn.close() return result is not None def get_weather(city): params = { 'q': city, 'appid': WEATHER_API_KEY, 'units': 'metric', 'lang': 'de' } response = requests.get(WEATHER_API_URL, params=params) if response.status_code == 200: data = response.json() current_temp = data['list'][0]['main']['temp'] weather_icon = map_weather_icon(data['list'][0]['weather'][0]['icon']) # Einfaches Forecast-Beispiel forecast = [] for i in range(0, len(data['list']), 8): day = data['list'][i] forecast.append({ 'date': day['dt_txt'].split()[0], 'day': { 'avgtemp_c': day['main']['temp'] }, 'weather_icon': map_weather_icon(day['weather'][0]['icon']) }) return current_temp, weather_icon, forecast else: return None, None, [] def map_weather_icon(icon_code): mapping = { '01d': 'fa-sun', '01n': 'fa-moon', '02d': 'fa-cloud-sun', '02n': 'fa-cloud-moon', '03d': 'fa-cloud', '03n': 'fa-cloud', '04d': 'fa-cloud-meatball', '04n': 'fa-cloud-meatball', '09d': 'fa-cloud-showers-heavy', '09n': 'fa-cloud-showers-heavy', '10d': 'fa-cloud-sun-rain', '10n': 'fa-cloud-moon-rain', '11d': 'fa-poo-storm', '11n': 'fa-poo-storm', '13d': 'fa-snowflake', '13n': 'fa-snowflake', '50d': 'fa-smog', '50n': 'fa-smog' } return mapping.get(icon_code, 'fa-cloud') def send_reset_email(to_email, reset_url): subject = "Passwort zurücksetzen" body = f""" Hallo, Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts erhalten. Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen: {reset_url} Der Link ist 48 Stunden gültig. Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie bitte diese E-Mail. Mit freundlichen Grüßen, Ihr ClickCandit Team """ msg = MIMEText(body, "plain") msg["Subject"] = subject msg["From"] = EMAIL_SENDER msg["To"] = to_email try: with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server: server.login(EMAIL_SENDER, EMAIL_PASSWORD) server.sendmail(EMAIL_SENDER, to_email, msg.as_string()) print(f"Passwort-Reset-E-Mail erfolgreich an {to_email} gesendet.") except Exception as e: print(f"Fehler beim Senden der E-Mail: {e}") def send_export_email(to_email, download_link): subject = "Ihre Nutzerdaten zum Download" body = f""" Hallo {to_email}, Sie haben den Export Ihrer Nutzerdaten angefordert. Klicken Sie auf den folgenden Link, um Ihre Daten herunterzuladen: {download_link} Dieser Link ist 48 Stunden gültig. Mit freundlichen Grüßen, Ihr ClickCandit Team """ msg = MIMEText(body, "plain") msg["Subject"] = subject msg["From"] = EMAIL_SENDER msg["To"] = to_email try: with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server: server.login(EMAIL_SENDER, EMAIL_PASSWORD) server.sendmail(EMAIL_SENDER, to_email, msg.as_string()) print(f"Download-Link an {to_email} gesendet.") except Exception as e: print(f"Fehler beim Senden der E-Mail: {e}") ###################################### # Routen ###################################### @app.route('/') def index(): # Wenn kein Admin existiert, zur Register-Seite (erster Admin) if not admin_exists(): return redirect(url_for('register')) # Wenn schon Admin da, aber keiner eingeloggt -> Login if 'user_id' not in session: return redirect(url_for('login')) # Ansonsten: Dashboard return redirect(url_for('dashboard')) @app.route('/register', methods=['GET', 'POST']) def register(): already_admin = admin_exists() if request.method == 'GET': # Wenn Admin existiert, nur eingeloggter Admin darf registrieren if already_admin: if 'role' not in session or session['role'] != 'admin': flash("Nur Admins können weitere Benutzer hinzufügen!", "danger") return redirect(url_for('login')) return render_template('register.html') # POST: Registrieren name = request.form['name'].strip() email = request.form['email'].strip() password = request.form['password'].strip() if not name or not email or not password: flash("Bitte alle Felder ausfüllen.", "danger") return redirect(url_for('register')) conn = get_db_connection() try: # Wenn noch kein Admin da -> erster wird Admin role = 'admin' if not already_admin else request.form.get('role', 'user') hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) conn.execute("INSERT INTO users (name, email, password, role) VALUES (?, ?, ?, ?)", (name, email, hashed_password, role)) conn.commit() flash("Benutzer erfolgreich registriert!", "success") except sqlite3.IntegrityError: flash("Diese E-Mail existiert bereits!", "danger") finally: conn.close() if not already_admin: # Erster Admin angelegt -> nun einloggen return redirect(url_for('login')) else: return redirect(url_for('admin')) @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': email = request.form['email'].strip() password = request.form['password'].strip() if not email or not password: flash("Bitte E-Mail und Passwort angeben!", "danger") return redirect(url_for('login')) conn = get_db_connection() user = conn.execute("SELECT * FROM users WHERE email = ?", (email,)).fetchone() conn.close() if user and bcrypt.checkpw(password.encode('utf-8'), user['password']): session['user_id'] = user['id'] session['user_name'] = user['name'] session['role'] = user['role'] flash("Login erfolgreich!", "success") return redirect(url_for('dashboard')) else: flash("Falsche E-Mail oder Passwort!", "danger") return redirect(url_for('login')) else: return render_template('login.html') @app.route('/logout') def logout(): session.clear() flash("Du wurdest ausgeloggt.", "info") return redirect(url_for('login')) @app.route('/dashboard') def dashboard(): if 'user_id' not in session: flash("Bitte melde dich an!", "danger") return redirect(url_for('login')) user_id = session['user_id'] conn = get_db_connection() user = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone() settings = conn.execute("SELECT * FROM settings WHERE user_id = ?", (user_id,)).fetchone() conn.close() if not user: flash("Benutzer nicht gefunden.", "danger") return redirect(url_for('logout')) # Default-Werte if settings is None: wallpaper = '19.png' city = 'Berlin' show_forecast = True bookmarks = [] else: wallpaper = settings['wallpaper'] city = settings['city'] show_forecast = bool(settings['show_forecast']) bookmarks = json.loads(settings['bookmarks']) current_temp, weather_icon, forecast = get_weather(city) if current_temp is None: current_temp = "N/A" weather_icon = "fa-question" forecast = [] domain = "example.com" logo_path = url_for('static', filename='clickcandit.png') # 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' }, ] ] return render_template( 'dashboard.html', user=user['name'], role=user['role'], # Für Admin-Abfrage im Template wallpaper=wallpaper, city=city, show_forecast=show_forecast, bookmarks=bookmarks, current_temp=current_temp, weather_icon=weather_icon, forecast=forecast, domain=domain, logo_path=logo_path, user_app_chunks=user_app_chunks ) ###################################### # Admin-Bereich ###################################### @app.route('/admin') def admin(): if 'role' not in session or session['role'] != 'admin': flash("Kein Zugriff!", "danger") return redirect(url_for('login')) conn = get_db_connection() users = conn.execute("SELECT id, name, email, role FROM users").fetchall() conn.close() return render_template('admin.html', users=users) @app.route('/admin/delete/', methods=['POST']) def delete_user(user_id): if 'role' not in session or session['role'] != 'admin': flash("Kein Zugriff!", "danger") return redirect(url_for('login')) conn = get_db_connection() conn.execute("DELETE FROM users WHERE id = ?", (user_id,)) conn.execute("DELETE FROM settings WHERE user_id = ?", (user_id,)) conn.commit() conn.close() flash("Benutzer gelöscht!", "success") return redirect(url_for('admin')) @app.route('/admin/edit_bookmarks/', methods=['GET', 'POST']) def edit_bookmarks(user_id): if 'role' not in session or session['role'] != 'admin': flash("Kein Zugriff", "danger") return redirect(url_for('login')) conn = get_db_connection() settings = conn.execute("SELECT * FROM settings WHERE user_id = ?", (user_id,)).fetchone() if request.method == 'POST': bookmarks_raw = request.form.get('bookmarks', '') bookmarks_list = [b.strip() for b in bookmarks_raw.split(',') if b.strip()] bookmarks_json = json.dumps(bookmarks_list) if settings: conn.execute("UPDATE settings SET bookmarks = ? WHERE user_id = ?", (bookmarks_json, user_id)) else: conn.execute("INSERT INTO settings (user_id, bookmarks) VALUES (?, ?)", (user_id, bookmarks_json)) conn.commit() conn.close() flash("Lesezeichen aktualisiert", "success") return redirect(url_for('admin')) else: current_bookmarks = json.loads(settings['bookmarks']) if settings else [] conn.close() return render_template('edit_bookmarks.html', user_id=user_id, bookmarks=current_bookmarks) ###################################### # Settings / API ###################################### @app.route('/save_settings', methods=['POST']) def save_settings(): if 'user_id' not in session: return jsonify({'success': False, 'message': 'Nicht autorisiert'}), 401 data = request.get_json() if not data: return jsonify({'success': False, 'message': 'Keine Daten übergeben'}), 400 wallpaper = data.get('wallpaper', '7.png') city = data.get('city', 'Berlin') bookmarks = json.dumps(data.get('bookmarks', [])) show_forecast = data.get('show_forecast', True) conn = get_db_connection() conn.execute(""" INSERT INTO settings (user_id, wallpaper, city, bookmarks, show_forecast) VALUES (?, ?, ?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET wallpaper=excluded.wallpaper, city=excluded.city, bookmarks=excluded.bookmarks, show_forecast=excluded.show_forecast """, (session['user_id'], wallpaper, city, bookmarks, show_forecast)) conn.commit() conn.close() return jsonify({'success': True}) @app.route('/get_settings', methods=['GET']) def get_settings(): if 'user_id' not in session: return jsonify({'success': False, 'message': 'Nicht autorisiert'}), 401 conn = get_db_connection() settings = conn.execute("SELECT * FROM settings WHERE user_id = ?", (session['user_id'],)).fetchone() conn.close() if settings: return jsonify({ 'wallpaper_url': url_for('static', filename=settings['wallpaper']), 'city': settings['city'], 'bookmarks': json.loads(settings['bookmarks']), 'show_forecast': bool(settings['show_forecast']) }) else: # Falls noch keine Settings da return jsonify({ 'wallpaper_url': url_for('static', filename='7.png'), 'city': 'Berlin', 'bookmarks': [], 'show_forecast': True }) ###################################### # Passwort / Account ###################################### @app.route('/forgot-password', methods=['GET', 'POST']) def forgot_password(): if request.method == 'POST': email = request.form['email'].strip() conn = get_db_connection() user = conn.execute("SELECT * FROM users WHERE email = ?", (email,)).fetchone() if not user: flash("Diese E-Mail ist nicht registriert.", "danger") conn.close() return redirect(url_for('forgot_password')) reset_token = secrets.token_hex(16) expiry_time = (datetime.utcnow() + timedelta(hours=48)).isoformat() conn.execute("UPDATE users SET reset_token=?, reset_token_expiry=? WHERE email=?", (reset_token, expiry_time, email)) conn.commit() conn.close() reset_url = url_for('reset_password', token=reset_token, _external=True) send_reset_email(email, reset_url) flash("Eine E-Mail zum Zurücksetzen des Passworts wurde gesendet.", "success") return redirect(url_for('login')) return render_template('forgot_password.html') @app.route('/reset-password/', methods=['GET', 'POST']) def reset_password(token): conn = get_db_connection() user = conn.execute("SELECT * FROM users WHERE reset_token = ?", (token,)).fetchone() if not user: conn.close() flash("Ungültiger Reset-Token", "danger") return redirect(url_for('forgot_password')) expiry = datetime.fromisoformat(user['reset_token_expiry']) if user['reset_token_expiry'] else None if not expiry or expiry < datetime.utcnow(): conn.close() flash("Reset-Link ist abgelaufen.", "danger") return redirect(url_for('forgot_password')) if request.method == 'POST': pw = request.form['password'].strip() cpw = request.form['confirm_password'].strip() if pw != cpw: flash("Passwörter stimmen nicht überein.", "danger") return render_template('reset_password.html', token=token) hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) conn.execute("UPDATE users SET password=?, reset_token=NULL, reset_token_expiry=NULL WHERE id=?", (hashed_pw, user['id'])) conn.commit() conn.close() flash("Ihr Passwort wurde zurückgesetzt.", "success") return redirect(url_for('login')) conn.close() return render_template('reset_password.html', token=token) @app.route('/delete_account', methods=['POST']) def delete_account(): if 'user_id' not in session: flash("Bitte melde dich an!", "danger") return redirect(url_for('login')) user_id = session['user_id'] try: conn = get_db_connection() user = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone() if not user: flash("Benutzerkonto nicht gefunden.", "danger") conn.close() return redirect(url_for('dashboard')) email = user["email"] conn.execute("DELETE FROM settings WHERE user_id=?", (user_id,)) conn.execute("DELETE FROM download_links WHERE user_id=?", (user_id,)) conn.execute("DELETE FROM users WHERE id=?", (user_id,)) conn.commit() conn.close() export_file = os.path.join(EXPORT_FOLDER, f"{email}_data.json") if os.path.exists(export_file): os.remove(export_file) session.clear() flash("Dein Konto wurde erfolgreich gelöscht.", "success") return redirect(url_for('login')) except Exception as e: print("Fehler beim Löschen des Kontos:", e) flash("Fehler beim Löschen des Kontos.", "danger") return redirect(url_for('dashboard')) ###################################### # Nutzerdaten-Export ###################################### @app.route('/request_data_export', methods=['GET']) def request_data_export(): if 'user_id' not in session: flash("Bitte melde dich an!", "danger") return redirect(url_for('login')) user_id = session['user_id'] conn = get_db_connection() user = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone() if not user: flash("Benutzerkonto nicht gefunden.", "danger") conn.close() return redirect(url_for('dashboard')) download_token = secrets.token_hex(16) expiry = (datetime.utcnow() + timedelta(hours=48)).isoformat() conn.execute("INSERT INTO download_links (user_id, token, expiration_time) VALUES (?, ?, ?)", (user_id, download_token, expiry)) conn.commit() conn.close() export_file = os.path.join(EXPORT_FOLDER, f"{user['email']}_data.json") user_data = { "name": user["name"], "email": user["email"], "role": user["role"] } with open(export_file, "w", encoding="utf-8") as f: json.dump(user_data, f, indent=4) download_url = url_for('download_user_data', token=download_token, _external=True) send_export_email(user["email"], download_url) flash("Eine E-Mail mit dem Download-Link wurde gesendet.", "success") return redirect(url_for('dashboard')) @app.route('/download_user_data/', methods=['GET']) def download_user_data(token): conn = get_db_connection() link_data = conn.execute("SELECT * FROM download_links WHERE token=?", (token,)).fetchone() if not link_data: conn.close() flash("Ungültiger oder abgelaufener Download-Link.", "danger") return redirect(url_for('dashboard')) expiry_time = datetime.fromisoformat(link_data["expiration_time"]) if expiry_time < datetime.utcnow(): conn.close() flash("Der Download-Link ist abgelaufen.", "danger") return redirect(url_for('dashboard')) user = conn.execute("SELECT * FROM users WHERE id=?", (link_data["user_id"],)).fetchone() conn.close() if not user: flash("Benutzerdaten nicht gefunden.", "danger") return redirect(url_for('dashboard')) export_file = os.path.join(EXPORT_FOLDER, f"{user['email']}_data.json") if not os.path.exists(export_file): flash("Exportdatei nicht gefunden.", "danger") return redirect(url_for('dashboard')) return send_file(export_file, as_attachment=True) if __name__ == '__main__': app.run(debug=True)