a46aa157f3
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Admin can no longer be picked as a red_tech/blue_tech assignee — it administers the site, it doesn't operate tests. Applied to the eligible-assignee list on both the picker UI and the assign endpoint's validation, and dropped from the GET /users/operators pool. push_assignee_update() now falls back to a fresh Jira account-id lookup when the assignee hasn't logged in yet (so jira_account_id is still empty), instead of silently no-op'ing — assignment now syncs to Jira immediately regardless of whether the assignee has ever logged into Aegis before.
244 lines
7.9 KiB
Python
244 lines
7.9 KiB
Python
"""User management router (admin only)."""
|
|
|
|
# Import uuid
|
|
import uuid
|
|
|
|
# Import APIRouter, Depends, status from fastapi
|
|
from fastapi import APIRouter, Depends, status
|
|
|
|
# Import Session from sqlalchemy.orm
|
|
from sqlalchemy.orm import Session
|
|
|
|
# Import get_db from app.database
|
|
from app.database import get_db
|
|
|
|
# Import require_role from app.dependencies.auth
|
|
from app.dependencies.auth import require_role, require_any_role_strict
|
|
|
|
# Import UnitOfWork from app.domain.unit_of_work
|
|
from app.domain.unit_of_work import UnitOfWork
|
|
|
|
# Import User from app.models.user
|
|
from app.models.user import User
|
|
from app.dependencies.auth import get_current_user
|
|
from app.schemas.user import UserCreate, UserUpdate, UserOut, UserPreferencesUpdate, UserOperatorOut
|
|
from app.services.audit_service import log_action
|
|
|
|
# Import from app.services.user_service
|
|
from app.services.user_service import (
|
|
create_user,
|
|
get_user_or_raise,
|
|
list_users,
|
|
update_user,
|
|
)
|
|
|
|
# Assign router = APIRouter(prefix="/users", tags=["users"])
|
|
router = APIRouter(prefix="/users", tags=["users"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PATCH /users/me/preferences — update current user preferences
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@router.patch("/me/preferences", response_model=UserOut)
|
|
def update_my_preferences(
|
|
payload: UserPreferencesUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""Update the current user's notification preferences, Jira account ID and Jira API token.
|
|
|
|
Send ``jira_api_token: ""`` to clear a previously stored token.
|
|
The token is never returned in any response.
|
|
"""
|
|
update_data = payload.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
if field in ("jira_api_token", "jira_email", "tempo_api_token"):
|
|
# Empty string means "clear the value"
|
|
setattr(current_user, field, value if value else None)
|
|
else:
|
|
setattr(current_user, field, value)
|
|
db.commit()
|
|
db.refresh(current_user)
|
|
return current_user
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /users/me — get current user's own profile
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@router.get("/me", response_model=UserOut)
|
|
def get_me(
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""Return the currently authenticated user's profile."""
|
|
return current_user
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /users — list all users
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@router.get("", response_model=list[UserOut])
|
|
# Define function list_users_route
|
|
def list_users_route(
|
|
# Entry: db
|
|
db: Session = Depends(get_db),
|
|
# Entry: current_user
|
|
current_user: User = Depends(require_role("admin")),
|
|
) -> list[UserOut]:
|
|
"""Return a list of all users. **Requires admin role.**."""
|
|
# Return list_users(db)
|
|
return list_users(db)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# POST /users — create a new user
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
|
|
# Define function create_user_route
|
|
def create_user_route(
|
|
# Entry: payload
|
|
payload: UserCreate,
|
|
# Entry: db
|
|
db: Session = Depends(get_db),
|
|
# Entry: current_user
|
|
current_user: User = Depends(require_role("admin")),
|
|
) -> UserOut:
|
|
"""Create a new user. **Requires admin role.**."""
|
|
# Open context manager
|
|
with UnitOfWork(db) as uow:
|
|
# Assign user = create_user(
|
|
user = create_user(
|
|
db,
|
|
# Keyword argument: username
|
|
username=payload.username,
|
|
# Keyword argument: email
|
|
email=payload.email,
|
|
# Keyword argument: password
|
|
password=payload.password,
|
|
# Keyword argument: role
|
|
role=payload.role,
|
|
)
|
|
# Call log_action()
|
|
log_action(
|
|
db,
|
|
# Keyword argument: user_id
|
|
user_id=current_user.id,
|
|
# Keyword argument: action
|
|
action="create_user",
|
|
# Keyword argument: entity_type
|
|
entity_type="user",
|
|
# Keyword argument: entity_id
|
|
entity_id=user.id,
|
|
# Keyword argument: details
|
|
details={"username": user.username, "role": user.role},
|
|
)
|
|
# Call uow.commit()
|
|
uow.commit()
|
|
# Reload ORM object attributes from the database
|
|
db.refresh(user)
|
|
|
|
# Return user
|
|
return user
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /users/operators — minimal operator/lead list for assignment pickers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@router.get("/operators", response_model=list[UserOperatorOut])
|
|
def list_operators_route(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_any_role_strict("manager", "red_lead", "blue_lead")),
|
|
) -> list[UserOperatorOut]:
|
|
"""Return active red/blue operators and leads, for lead/manager assignment pickers.
|
|
|
|
Not reachable by admin — admin administers the site, leads/managers
|
|
coordinate operators, and admin cannot itself be assigned as an
|
|
operator either. Returns only id/username/role — no emails or
|
|
tokens — since this is reachable by non-admin leads.
|
|
"""
|
|
return (
|
|
db.query(User)
|
|
.filter(
|
|
User.role.in_(["red_tech", "red_lead", "blue_tech", "blue_lead"]),
|
|
User.is_active.is_(True),
|
|
)
|
|
.order_by(User.username)
|
|
.all()
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /users/{id} — get a single user
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@router.get("/{user_id}", response_model=UserOut)
|
|
# Define function get_user
|
|
def get_user(
|
|
# Entry: user_id
|
|
user_id: uuid.UUID,
|
|
# Entry: db
|
|
db: Session = Depends(get_db),
|
|
# Entry: current_user
|
|
current_user: User = Depends(require_role("admin")),
|
|
) -> UserOut:
|
|
"""Return a single user by ID. **Requires admin role.**."""
|
|
# Return get_user_or_raise(db, user_id)
|
|
return get_user_or_raise(db, user_id)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PATCH /users/{id} — update a user
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@router.patch("/{user_id}", response_model=UserOut)
|
|
# Define function update_user_route
|
|
def update_user_route(
|
|
# Entry: user_id
|
|
user_id: uuid.UUID,
|
|
# Entry: payload
|
|
payload: UserUpdate,
|
|
# Entry: db
|
|
db: Session = Depends(get_db),
|
|
# Entry: current_user
|
|
current_user: User = Depends(require_role("admin")),
|
|
) -> UserOut:
|
|
"""Update one or more fields of an existing user. **Requires admin role.**."""
|
|
# Assign update_data = payload.model_dump(exclude_unset=True)
|
|
update_data = payload.model_dump(exclude_unset=True)
|
|
# Open context manager
|
|
with UnitOfWork(db) as uow:
|
|
# Assign user = update_user(db, user_id, **update_data)
|
|
user = update_user(db, user_id, **update_data)
|
|
# Call log_action()
|
|
log_action(
|
|
db,
|
|
# Keyword argument: user_id
|
|
user_id=current_user.id,
|
|
# Keyword argument: action
|
|
action="update_user",
|
|
# Keyword argument: entity_type
|
|
entity_type="user",
|
|
# Keyword argument: entity_id
|
|
entity_id=user.id,
|
|
# Keyword argument: details
|
|
details={"updated_fields": list(update_data.keys())},
|
|
)
|
|
# Call uow.commit()
|
|
uow.commit()
|
|
# Reload ORM object attributes from the database
|
|
db.refresh(user)
|
|
|
|
# Return user
|
|
return user
|