"""Auth routes: login, current user, first-time admin setup. No self-registration.""" from __future__ import annotations from flask import Blueprint, jsonify, request from ..auth.users import AuthError from .helpers import ApiError, current_user, require_auth auth_bp = Blueprint("auth", __name__) def _store(): from flask import current_app return current_app.extensions["user_store"] def _login_body(data: dict) -> str: # Accept `username` (primary) or `email` (fallback), lower-cased. return (data.get("username") or data.get("email") or "").strip().lower() @auth_bp.post("/login") def login(): data = request.get_json(silent=True) or {} username = _login_body(data) password = data.get("password") or "" if not username or not password: raise ApiError("username and password are required") try: user = _store().verify(username, password) token = _store().issue_token(user) except AuthError as exc: raise ApiError(str(exc), 401) return jsonify({ "token": token, "user": _store().public_user(user), "must_setup": bool(user.get("must_setup")), }) @auth_bp.get("/me") @require_auth def me(): return jsonify({"user": _store().public_user(current_user())}) @auth_bp.post("/setup") @require_auth def setup(): """First-time admin setup: set email + change password, then clear must_setup.""" user = current_user() data = request.get_json(silent=True) or {} username = (data.get("username") or user.get("username") or user.get("id") or "").strip().lower() email = (data.get("email") or "").strip() new_password = data.get("password") or "" if not email or not new_password: raise ApiError("email and new password are required") try: updated = _store().complete_setup(username, email, new_password) except AuthError as exc: raise ApiError(str(exc), 400) return jsonify({ "ok": True, "user": _store().public_user(updated), "must_setup": False, }) @auth_bp.post("/change-password") @require_auth def change_password(): """Change own password — requires the current password to be correct.""" user = current_user() data = request.get_json(silent=True) or {} current_pw = data.get("current_password") or "" new_pw = data.get("new_password") or "" if not current_pw or not new_pw: raise ApiError("current and new password are required") # verify the current password first try: _store().verify(user["username"], current_pw) except AuthError: raise ApiError("current password is incorrect", 400) if len(new_pw) < 4: raise ApiError("new password must be at least 4 characters", 400) updated = _store().set_password(user["username"], new_pw) return jsonify({"ok": True, "user": _store().public_user(updated)}) @auth_bp.put("/profile") @require_auth def update_profile(): """Update own profile: display name and/or email.""" user = current_user() data = request.get_json(silent=True) or {} updates = {} if "name" in data: name = (data.get("name") or "").strip() if name: updates["name"] = name _store().users.update(user["username"], name=name) if "email" in data: email = (data.get("email") or "").strip() try: _store().set_email(user["username"], email) except AuthError as exc: raise ApiError(str(exc), 400) updates["email"] = (email or "").lower() if not updates: raise ApiError("nothing to update", 400) updated = _store().get_user(user["username"]) return jsonify({"ok": True, "user": _store().public_user(updated)})