implement client users
This commit is contained in:
73
app.py
73
app.py
@@ -7,6 +7,7 @@ from urllib.parse import urlparse
|
|||||||
from flask import (
|
from flask import (
|
||||||
Flask, render_template, request, redirect, url_for, flash, session, jsonify, abort
|
Flask, render_template, request, redirect, url_for, flash, session, jsonify, abort
|
||||||
)
|
)
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Config
|
# Config
|
||||||
@@ -121,6 +122,39 @@ def create_app():
|
|||||||
rows = conn.execute(query).fetchall()
|
rows = conn.execute(query).fetchall()
|
||||||
return render_template("apps_list.html", apps=rows)
|
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"])
|
@app.route("/apps/new", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def app_create():
|
def app_create():
|
||||||
@@ -221,6 +255,36 @@ def create_app():
|
|||||||
# ---------------------------
|
# ---------------------------
|
||||||
# API Endpoint (public or protected — here public for simplicity)
|
# 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"])
|
@app.route("/api/apps", methods=["GET"])
|
||||||
def api_apps():
|
def api_apps():
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
@@ -256,6 +320,13 @@ CREATE TABLE IF NOT EXISTS apps (
|
|||||||
priority INTEGER NOT NULL DEFAULT 0,
|
priority INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at TEXT NOT NULL
|
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)
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with sqlite3.connect(DB_PATH) as conn:
|
with sqlite3.connect(DB_PATH) as conn:
|
||||||
conn.execute("PRAGMA journal_mode=WAL;")
|
conn.execute("PRAGMA journal_mode=WAL;")
|
||||||
conn.execute(SCHEMA_SQL)
|
conn.executescript(SCHEMA_SQL)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<nav>
|
<nav>
|
||||||
{% if session.get('user') %}
|
{% if session.get('user') %}
|
||||||
<a href="{{ url_for('apps_list') }}">Home</a>
|
<a href="{{ url_for('apps_list') }}">Home</a>
|
||||||
|
<a href="{{ url_for('user_create') }}">Create User</a>
|
||||||
<a href="{{ url_for('logout') }}">Logout</a>
|
<a href="{{ url_for('logout') }}">Logout</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
17
templates/create_user.html
Normal file
17
templates/create_user.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Create User – LaunchPad Admin{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Create User</h1>
|
||||||
|
<form method="post" class="form">
|
||||||
|
<label>Username
|
||||||
|
<input type="text" name="username" required autofocus />
|
||||||
|
</label>
|
||||||
|
<label>Password
|
||||||
|
<input type="password" name="password" minlength="6" required />
|
||||||
|
</label>
|
||||||
|
<div class="form-actions">
|
||||||
|
<a class="btn" href="{{ url_for('apps_list') }}">Cancel</a>
|
||||||
|
<button class="btn primary" type="submit">Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user