diff --git a/app.py b/app.py index b037c7b..0ce5773 100644 --- a/app.py +++ b/app.py @@ -7,6 +7,7 @@ from urllib.parse import urlparse from flask import ( Flask, render_template, request, redirect, url_for, flash, session, jsonify, abort ) +from werkzeug.security import generate_password_hash, check_password_hash # ---------------------------------------------------------------------------- # Config @@ -121,6 +122,39 @@ def create_app(): rows = conn.execute(query).fetchall() return render_template("apps_list.html", apps=rows) + @app.route("/admin/users/new", methods=["GET", "POST"]) + @login_required + def user_create(): + """Admin page to create application users (username + password).""" + if request.method == "POST": + username = (request.form.get("username", "").strip()).lower() + password = request.form.get("password", "") + + # Basic validations + if not username: + flash("Username is required", "danger") + return render_template("create_user.html") + if not password or len(password) < 6: + flash("Password must be at least 6 characters", "danger") + return render_template("create_user.html") + + pwd_hash = generate_password_hash(password) + try: + with get_db() as conn: + conn.execute( + "INSERT INTO users (username, password_hash, created_at) VALUES (?,?,?)", + (username, pwd_hash, datetime.utcnow().isoformat(timespec='seconds')), + ) + conn.commit() + flash("User created", "success") + return redirect(url_for("apps_list")) + except sqlite3.IntegrityError: + flash("Username already exists", "danger") + return render_template("create_user.html") + + # GET + return render_template("create_user.html") + @app.route("/apps/new", methods=["GET", "POST"]) @login_required def app_create(): @@ -221,6 +255,36 @@ def create_app(): # --------------------------- # API Endpoint (public or protected — here public for simplicity) # --------------------------- + @app.route("/api/login", methods=["POST"]) + def api_login(): + """Simple login endpoint for the Android app. + Expects JSON: {"username": "...", "password": "..."} + Returns 200 on success with optional token field, 401 on failure. + """ + try: + payload = request.get_json(silent=True) or {} + username = (payload.get("username") or "").strip().lower() + password = payload.get("password") or "" + except Exception: + return jsonify({"error": "Invalid JSON"}), 400 + + if not username or not password: + return jsonify({"error": "username and password are required"}), 400 + + with sqlite3.connect(DB_PATH) as conn: + conn.row_factory = sqlite3.Row + row = conn.execute( + "SELECT id, username, password_hash FROM users WHERE username=?", + (username,), + ).fetchone() + + if not row or not check_password_hash(row["password_hash"], password): + return jsonify({"error": "Invalid credentials"}), 401 + + # For now we do not issue a JWT; Android accepts null/absent token. + # If you later add JWT, return it here as {"token": "..."} + return jsonify({"token": None}), 200 + @app.route("/api/apps", methods=["GET"]) def api_apps(): with get_db() as conn: @@ -256,6 +320,13 @@ CREATE TABLE IF NOT EXISTS apps ( priority INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL ); + +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + created_at TEXT NOT NULL +); """ @@ -263,7 +334,7 @@ def init_db(): DB_PATH.parent.mkdir(parents=True, exist_ok=True) with sqlite3.connect(DB_PATH) as conn: conn.execute("PRAGMA journal_mode=WAL;") - conn.execute(SCHEMA_SQL) + conn.executescript(SCHEMA_SQL) conn.commit() diff --git a/templates/base.html b/templates/base.html index bd3f7b8..f1853f7 100644 --- a/templates/base.html +++ b/templates/base.html @@ -12,6 +12,7 @@ diff --git a/templates/create_user.html b/templates/create_user.html new file mode 100644 index 0000000..c882929 --- /dev/null +++ b/templates/create_user.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block title %}Create User – LaunchPad Admin{% endblock %} +{% block content %} +