Files
sales-trainer/backend/app/api/auth_routes.py
Macky fc041b8f37 UI: 3-tab layout (Admin overview / My dashboard / Training) + Settings page
Frontend:
- App shell with 3 tabs: Overview (admin), My Dashboard (all incl admin), Training + Settings icon
- New Settings.vue: edit profile (name/email) + change password (verify current)
- New MyBoard.vue: personal win/lose + weak areas + summary (all roles)
- New Training.vue: product/group list -> personas; admin manage + add product
- Dashboard.vue -> admin overview with date-range filter + per-trainee stats
- Added i18n keys (EN/TH) and tab/settings styling (ui-ux-pro-max design system)

Backend:
- /api/auth/change-password (verify current pw) + PUT /api/auth/profile
- Allow admin to train: board/weak-areas/chat/personas/sessions open to user+admin+super_admin
- /api/analytics now accepts ?from=&to= date filter + returns per_user summary

Verified: frontend builds; change-password (wrong/ok), profile, board all pass via test client; security tests pass
2026-08-07 19:11:11 +07:00

114 lines
3.7 KiB
Python

"""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)})