add auth support

This commit is contained in:
Mateusz Gruszczyński
2026-05-06 08:38:07 +02:00
parent aea3c92830
commit dc1cac4e6f
20 changed files with 1185 additions and 220 deletions

View File

@@ -1,11 +1,34 @@
from __future__ import annotations
from flask import Blueprint, render_template, jsonify, Response
from flask import Blueprint, render_template, jsonify, Response, request, redirect, url_for, abort
from ..services.preferences import get_preferences, list_profiles, active_profile, BOOTSTRAP_THEMES, FONT_FAMILIES, bootstrap_css_url
from ..services import auth
bp = Blueprint("main", __name__)
@bp.route("/login", methods=["GET", "POST"])
def login():
# Note: When optional authentication is disabled, /login is intentionally unavailable.
if not auth.enabled():
abort(404)
error = ""
if request.method == "POST":
user = auth.login_user(request.form.get("username", ""), request.form.get("password", ""))
if user:
return redirect(request.args.get("next") or url_for("main.index"))
error = "Invalid username or password"
return render_template("login.html", error=error)
@bp.get("/logout")
def logout():
auth.logout_user()
if not auth.enabled():
return redirect(url_for("main.index"))
return redirect(url_for("main.login"))
@bp.get("/")
def index():
prefs = get_preferences()
@@ -17,6 +40,8 @@ def index():
bootstrap_themes=BOOTSTRAP_THEMES,
font_families=FONT_FAMILIES,
bootstrap_css_url=bootstrap_css_url((prefs or {}).get("bootstrap_theme")),
auth_enabled=auth.enabled(),
current_user=auth.current_user(),
)
@@ -80,7 +105,13 @@ def openapi():
"/api/traffic/history": {"get": {"summary": "Transfer history for charts", "parameters": [{"name": "range", "in": "query", "schema": {"type": "string", "enum": ["15m", "1h", "3h", "6h", "24h", "7d", "30d", "90d"]}}], "responses": {"200": {"description": "Aggregated traffic history"}}}}
}
paths.update({
"/api/auth/login": {"post": {"summary": "Log in with username and password when authentication is enabled", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"username": {"type": "string"}, "password": {"type": "string", "format": "password"}}, "required": ["username", "password"]}}}}, "responses": {"200": {"description": "Logged in"}, "401": {"description": "Invalid credentials"}, "404": {"description": "Authentication disabled"}}}},
"/api/auth/me": {"get": {"summary": "Return current authenticated user", "responses": {"200": {"description": "Current user"}, "404": {"description": "Authentication disabled"}}}},
"/api/auth/logout": {"post": {"summary": "Log out current user", "responses": {"200": {"description": "Logged out"}, "404": {"description": "Authentication disabled"}}}},
"/api/auth/users": {"get": {"summary": "List users, admin only", "responses": {"200": {"description": "Users"}, "403": {"description": "Admin only"}}}, "post": {"summary": "Create user, admin only", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthUserInput"}}}}, "responses": {"200": {"description": "User created"}, "403": {"description": "Admin only"}}}},
"/api/auth/users/{user_id}": {"put": {"summary": "Update user, admin only", "parameters": [{"name": "user_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthUserInput"}}}}, "responses": {"200": {"description": "User updated"}, "403": {"description": "Admin only"}}}, "delete": {"summary": "Delete user, admin only", "parameters": [{"name": "user_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "responses": {"200": {"description": "User deleted"}, "403": {"description": "Admin only"}}}},
"/api/profiles/{profile_id}": {"delete": {"summary": "Delete rTorrent profile", "parameters": [{"name": "profile_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "responses": {"200": {"description": "Deleted"}}}},
"/api/torrent-stats": {"get": {"summary": "Torrent statistics and cached file metadata", "parameters": [{"name": "force", "in": "query", "schema": {"type": "boolean", "default": False}}], "responses": {"200": {"description": "Torrent statistics"}}}},
"/api/path/default": {"get": {"summary": "Read active rTorrent default download path", "responses": {"200": {"description": "Default path"}}}},
"/api/torrents/{torrent_hash}/files/priority": {"post": {"summary": "Set file priorities", "parameters": [{"name": "torrent_hash", "in": "path", "required": True, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"files": {"type": "array", "items": {"type": "object", "properties": {"index": {"type": "integer"}, "priority": {"type": "integer", "enum": [0, 1, 2]}}}}}}}}}, "responses": {"200": {"description": "Updated priorities"}, "207": {"description": "Partial update"}}}},
"/api/torrents/{torrent_hash}/peers/action": {"post": {"summary": "Run peer action", "parameters": [{"name": "torrent_hash", "in": "path", "required": True, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"peer_index": {"type": "integer"}, "action": {"type": "string", "enum": ["disconnect", "kick", "snub", "unsnub", "ban"]}}}}}}, "responses": {"200": {"description": "Peer action result"}}}},
@@ -98,6 +129,17 @@ def openapi():
"properties": {"ok": {"type": "boolean"}},
"required": ["ok"],
},
"AuthUserInput": {
"type": "object",
"properties": {
"username": {"type": "string"},
"password": {"type": "string", "format": "password", "description": "Optional on update"},
"role": {"type": "string", "enum": ["admin", "user"]},
"is_active": {"type": "boolean"},
"permissions": {"type": "array", "items": {"type": "object", "properties": {"profile_id": {"type": "integer", "description": "0 means all profiles"}, "access_level": {"type": "string", "enum": ["ro", "full"]}}}},
},
"required": ["username"],
},
"Profile": {
"type": "object",
"additionalProperties": True,
@@ -278,4 +320,9 @@ def openapi():
},
})
components.setdefault("securitySchemes", {})["sessionCookie"] = {"type": "apiKey", "in": "cookie", "name": "session"}
for path, methods in paths.items():
if path != "/api/auth/login":
for operation in methods.values():
operation.setdefault("security", [{"sessionCookie": []}])
return jsonify({"openapi": "3.0.3", "info": {"title": "pyTorrent API", "version": "0.2.0"}, "paths": paths, "components": components})