From 394c27401d2c2b2d0b6abecffb464642cdbf4a47 Mon Sep 17 00:00:00 2001 From: wander Date: Sun, 8 Mar 2026 04:00:14 -0400 Subject: [PATCH] feat: implement session-based authentication and modern login UI --- ta_symlink.py | 45 +++++++++---- ui/src/components/Dashboard.svelte | 4 ++ ui/src/layouts/Layout.astro | 17 +++++ ui/src/pages/login.astro | 101 +++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 ui/src/pages/login.astro diff --git a/ta_symlink.py b/ta_symlink.py index 81e3651..21fd233 100644 --- a/ta_symlink.py +++ b/ta_symlink.py @@ -9,7 +9,7 @@ import ipaddress import shutil from datetime import datetime from functools import wraps -from flask import Flask, jsonify, render_template, request, abort, Response, send_from_directory +from flask import Flask, jsonify, render_template, request, abort, Response, send_from_directory, session, redirect, url_for # Load config from environment variables API_URL = os.getenv("API_URL", "http://localhost:8457/api") @@ -28,6 +28,7 @@ HEADERS = {"Authorization": f"Token {API_TOKEN}"} # Serve static files from ui/dist STATIC_FOLDER = os.path.join(os.getcwd(), 'ui', 'dist') app = Flask(__name__, static_folder=STATIC_FOLDER, static_url_path='/') +app.secret_key = os.getenv("FLASK_SECRET_KEY", "tubesortermagicpika") # Change in production! # Database setup import sqlite3 @@ -918,22 +919,44 @@ def check_auth(username, password): """Checks whether a username/password combination is valid.""" return username == UI_USERNAME and password == UI_PASSWORD -def authenticate(): - """Sends a 401 response that enables basic auth""" - return Response( - 'Could not verify your access level for that URL.\n' - 'You have to login with proper credentials', 401, - {'WWW-Authenticate': 'Basic realm="Login Required"'}) - def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): - auth = request.authorization - if not auth or not check_auth(auth.username, auth.password): - return authenticate() + if not session.get('logged_in'): + if request.path.startswith('/api/'): + return jsonify({"error": "Unauthorized"}), 401 + return redirect('/login') return f(*args, **kwargs) return decorated +@app.route("/api/auth/login", methods=["POST"]) +def api_login(): + data = request.json + username = data.get("username") + password = data.get("password") + + if check_auth(username, password): + session['logged_in'] = True + session['username'] = username + return jsonify({"success": True}) + return jsonify({"error": "Invalid credentials"}), 401 + +@app.route("/api/auth/logout", methods=["POST"]) +def api_logout(): + session.clear() + return jsonify({"success": True}) + +@app.route("/api/auth/status") +def api_auth_status(): + return jsonify({ + "logged_in": session.get('logged_in', False), + "username": session.get('username') + }) + +@app.route("/login") +def login_page(): + return send_from_directory(app.static_folder, 'login/index.html') + @app.route("/") @requires_auth def index(): diff --git a/ui/src/components/Dashboard.svelte b/ui/src/components/Dashboard.svelte index c694fc0..29181ae 100644 --- a/ui/src/components/Dashboard.svelte +++ b/ui/src/components/Dashboard.svelte @@ -24,6 +24,10 @@ async function fetchData() { try { const res = await fetch("/api/status"); + if (res.status === 401) { + window.location.href = "/login"; + return; + } if (!res.ok) throw new Error("Failed to fetch status"); const data = await res.json(); diff --git a/ui/src/layouts/Layout.astro b/ui/src/layouts/Layout.astro index 0f94ddb..5712307 100644 --- a/ui/src/layouts/Layout.astro +++ b/ui/src/layouts/Layout.astro @@ -70,6 +70,12 @@ const { title } = Astro.props; > Transcoding +
@@ -95,5 +101,16 @@ const { title } = Astro.props; >

TUBESORTER // SYSTEM_V2 // BUN_POWERED

+ diff --git a/ui/src/pages/login.astro b/ui/src/pages/login.astro new file mode 100644 index 0000000..ff175d3 --- /dev/null +++ b/ui/src/pages/login.astro @@ -0,0 +1,101 @@ +--- +import Layout from "../layouts/Layout.astro"; +--- + + +
+
+ +
+
+ +
+
+ +

+ ACCESS_REQUIRED +

+

+ // PLEASE_IDENTIFY_YOURSELF +

+
+ +
+
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+
+ + + +
+ +
+
+
+
+
+
+ +