From c237564f61cdbc5f2516199348cae8a8c3d9201d Mon Sep 17 00:00:00 2001 From: zwitschi Date: Fri, 24 Jul 2026 23:59:03 +0200 Subject: [PATCH] Add login functionality with password protection and create login page --- .env.example | 4 +- static/css/style.css | 114 +++++++++++++++++++++++++++++++++++++++++++ templates/login.html | 43 ++++++++++++++++ videoplayer.py | 46 ++++++++++++++++- 4 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 templates/login.html diff --git a/.env.example b/.env.example index ad4741d..2399114 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,6 @@ FLASK_DEBUG=0 # Seconds to cache the TeslaCam directory scan (default 300). Hit /refresh to # force a re-scan. -SCAN_CACHE_TTL=3600 \ No newline at end of file +SCAN_CACHE_TTL=3600 + +LOGIN_PASSWORD=patreon \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index fbe8562..f00550e 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -520,3 +520,117 @@ header { [hidden] { display: none !important; } + +/* ============================================= + Login Page + ============================================= */ +.login-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background: radial-gradient( + ellipse at 50% 30%, + #1a1a22 0%, + var(--bg-primary) 70% + ); +} + +.login-card { + width: 100%; + max-width: 380px; + padding: 40px 36px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); +} + +.login-brand { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + margin-bottom: 32px; +} + +.login-brand .logo { + width: 48px; + height: 48px; + display: grid; + place-items: center; + color: var(--accent); +} + +.login-brand .logo svg { + width: 100%; + height: 100%; + fill: var(--accent); +} + +.login-brand h1 { + font-size: 20px; + font-weight: 700; + letter-spacing: 0.5px; + color: var(--text-primary); +} + +.login-form { + display: flex; + flex-direction: column; + gap: 14px; +} + +.login-form label { + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.8px; +} + +.login-form input[type="password"] { + width: 100%; + padding: 12px 16px; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 15px; + outline: none; + transition: + border-color 0.2s, + box-shadow 0.2s; +} + +.login-form input[type="password"]:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(227, 25, 55, 0.15); +} + +.login-form button { + width: 100%; + padding: 12px; + background: var(--accent); + border: none; + border-radius: var(--radius-sm); + color: #fff; + font-size: 15px; + font-weight: 700; + cursor: pointer; + transition: background 0.15s; + margin-top: 4px; +} + +.login-form button:hover { + background: var(--accent-hover); +} + +.login-error { + font-size: 13px; + color: var(--accent); + text-align: center; + padding: 8px; + background: rgba(227, 25, 55, 0.08); + border-radius: var(--radius-sm); +} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..39e957e --- /dev/null +++ b/templates/login.html @@ -0,0 +1,43 @@ + + + + + + TeslaCam Player — Login + + + +
+ + + +
+ + diff --git a/videoplayer.py b/videoplayer.py index 6a52fc7..09ed417 100644 --- a/videoplayer.py +++ b/videoplayer.py @@ -12,15 +12,26 @@ play videos from different camera views. import os import re import time +import secrets +from functools import wraps from dotenv import load_dotenv -from flask import Flask, render_template, send_from_directory, abort, jsonify +from flask import ( + Flask, render_template, send_from_directory, abort, jsonify, + request, session, redirect, url_for +) # Load variables from a .env file if present (does not override real env vars) load_dotenv() app = Flask(__name__) +# Session secret key — MUST be set in production. Generate with: python -c "import secrets; print(secrets.token_hex(32))" +app.secret_key = os.environ.get('SECRET_KEY', secrets.token_hex(32)) + +# Password for web login. Default: 'patreon'. Override via LOGIN_PASSWORD env var. +LOGIN_PASSWORD = os.environ.get('LOGIN_PASSWORD', 'patreon') + # Root TeslaCam directory containing RecentClips, SavedClips, SentryClips, etc. # Override via the TESLACAM_DIRECTORY environment variable (see .env.example). TESLACAM_DIRECTORY = os.environ.get('TESLACAM_DIRECTORY', '/mnt/TeslaCam') @@ -113,7 +124,37 @@ def get_camera_display_name(camera_view): return CAMERA_VIEW_MAPPING.get(camera_view, camera_view.replace('_', ' ').title()) +def login_required(f): + """Decorator: redirect to /login if not authenticated.""" + @wraps(f) + def decorated_function(*args, **kwargs): + if not session.get('logged_in'): + return redirect(url_for('login')) + return f(*args, **kwargs) + return decorated_function + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + """Simple password-based login page.""" + error = None + if request.method == 'POST': + if request.form.get('password') == LOGIN_PASSWORD: + session['logged_in'] = True + return redirect(url_for('index')) + error = 'Invalid password. Please try again.' + return render_template('login.html', error=error) + + +@app.route('/logout') +def logout(): + """Clear session and return to login page.""" + session.pop('logged_in', None) + return redirect(url_for('login')) + + @app.route('/') +@login_required def index(): """ Main page listing all videos categorized by human-readable camera view. @@ -133,6 +174,7 @@ def index(): @app.route('/camera/') +@login_required def camera_view(camera_view): """ Displays all available videos for a single camera view. @@ -150,6 +192,7 @@ def camera_view(camera_view): @app.route('/video/') +@login_required def serve_video(filepath): """ Safely streams video files from the TeslaCam directory structure. @@ -162,6 +205,7 @@ def serve_video(filepath): @app.route('/refresh') +@login_required def refresh(): """ Forces a re-scan of the TeslaCam directory, bypassing the cache TTL.