Milestone 3
This commit is contained in:
79
backend/app/api/setup.py
Normal file
79
backend/app/api/setup.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Bootstrap endpoint — consumes the install token to create the first admin."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, jsonify, make_response, request
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
from app.api._validation import Email
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.rate_limit import limiter
|
||||
from app.db.session import session_scope
|
||||
from app.models.auth import User
|
||||
from app.services.bootstrap import (
|
||||
BootstrapError,
|
||||
bootstrap_admin,
|
||||
ensure_system_groups,
|
||||
)
|
||||
|
||||
bp = Blueprint("setup", __name__, url_prefix="/setup")
|
||||
log = logging.getLogger("metamorph.api.setup")
|
||||
|
||||
|
||||
class SetupPayload(BaseModel):
|
||||
install_token: str = Field(min_length=20)
|
||||
email: Email
|
||||
password: str = Field(min_length=8)
|
||||
display_name: str | None = None
|
||||
|
||||
|
||||
@bp.get("")
|
||||
def setup_status():
|
||||
"""Tell the SPA whether the bootstrap has already been done.
|
||||
|
||||
Used by the front to redirect to /setup vs /login on first paint.
|
||||
"""
|
||||
with session_scope() as s:
|
||||
any_user = s.scalar(select(User.id).limit(1)) is not None
|
||||
return jsonify({"completed": any_user})
|
||||
|
||||
|
||||
@bp.post("")
|
||||
@limiter.limit("5 per minute")
|
||||
def setup():
|
||||
try:
|
||||
payload = SetupPayload.model_validate(request.get_json(silent=True) or {})
|
||||
except ValidationError as e:
|
||||
return jsonify({"error": "invalid_request", "details": e.errors()}), 400
|
||||
|
||||
try:
|
||||
result = bootstrap_admin(
|
||||
install_token=payload.install_token,
|
||||
email=payload.email,
|
||||
password=payload.password,
|
||||
display_name=payload.display_name,
|
||||
)
|
||||
except BootstrapError as e:
|
||||
return jsonify({"error": "bootstrap_failed", "message": str(e)}), 409
|
||||
except ValueError as e:
|
||||
return jsonify({"error": "invalid_request", "message": str(e)}), 400
|
||||
|
||||
log.warning(
|
||||
"metamorph.bootstrap.completed",
|
||||
extra={"user_id": str(result.user_id), "admin_group_id": str(result.admin_group_id)},
|
||||
)
|
||||
# Make sure the redteam/blueteam groups exist too (idempotent).
|
||||
ensure_system_groups()
|
||||
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"user_id": str(result.user_id),
|
||||
}
|
||||
),
|
||||
201,
|
||||
)
|
||||
Reference in New Issue
Block a user