feat(tests,users): blocking procedure-suggestion review on test detail, multi-role switcher, Power Automate webhook payload
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
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
- A lead opening a test with a pending procedure suggestion awaiting
their review now gets a blocking popup (approve/reject only) instead
of discovering it later in a separate queue.
- Users can be granted more than one role via extra_roles; only one is
ever active at a time (no permission mixing) and a top-bar switcher
lets the user swap which one, taking effect immediately since role
is read fresh from the DB on every request.
- The password-setup/reset webhook now posts the agreed Power Automate
contract ({to, subject, body} with the platform's standard greeting
and signature template) and supports an admin-configured API key
sent as an x-api-key header.
This commit is contained in:
@@ -73,6 +73,25 @@ def list_suggestions(
|
||||
return [_to_out(s, template_by_id) for s in suggestions]
|
||||
|
||||
|
||||
@router.get("/for-test/{test_id}", response_model=list[ProcedureSuggestionOut])
|
||||
def list_suggestions_for_test(
|
||||
test_id: uuid.UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_any_role_strict("red_lead", "blue_lead", "admin")),
|
||||
) -> list[ProcedureSuggestionOut]:
|
||||
"""List pending suggestions tied to a specific test, scoped to the
|
||||
caller's team (admins see both) — powers the blocking review popup a
|
||||
lead sees when opening a test that has one awaiting them."""
|
||||
team = _team_for_role(current_user)
|
||||
suggestions = list_pending_suggestions(db, team=team, source_test_id=test_id)
|
||||
template_ids = {s.template_id for s in suggestions}
|
||||
templates = (
|
||||
db.query(TestTemplate).filter(TestTemplate.id.in_(template_ids)).all() if template_ids else []
|
||||
)
|
||||
template_by_id = {t.id: t for t in templates}
|
||||
return [_to_out(s, template_by_id) for s in suggestions]
|
||||
|
||||
|
||||
@router.post("/{suggestion_id}/approve", response_model=ProcedureSuggestionOut)
|
||||
def approve_suggestion(
|
||||
suggestion_id: uuid.UUID,
|
||||
|
||||
@@ -347,10 +347,13 @@ def update_jira_config(
|
||||
class PasswordWebhookConfigOut(BaseModel):
|
||||
configured: bool
|
||||
url: str
|
||||
api_key_set: bool
|
||||
|
||||
|
||||
class PasswordWebhookConfigUpdate(BaseModel):
|
||||
url: str
|
||||
# Optional — omit or send "" to leave the currently-stored key unchanged.
|
||||
api_key: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/password-webhook-config", response_model=PasswordWebhookConfigOut)
|
||||
@@ -360,12 +363,15 @@ def get_password_webhook_config(
|
||||
):
|
||||
"""Return the configured webhook URL used to email set-password links.
|
||||
|
||||
The API key itself is never returned, only whether one is set.
|
||||
|
||||
**Requires** the ``admin`` role.
|
||||
"""
|
||||
from app.services.password_setup_service import get_password_webhook_url
|
||||
from app.services.password_setup_service import get_password_webhook_api_key, get_password_webhook_url
|
||||
|
||||
url = get_password_webhook_url(db) or ""
|
||||
return PasswordWebhookConfigOut(configured=bool(url), url=url)
|
||||
api_key_set = bool(get_password_webhook_api_key(db))
|
||||
return PasswordWebhookConfigOut(configured=bool(url), url=url, api_key_set=api_key_set)
|
||||
|
||||
|
||||
@router.patch("/password-webhook-config", response_model=PasswordWebhookConfigOut)
|
||||
@@ -374,16 +380,24 @@ def update_password_webhook_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""Set the webhook URL used to email set-password links.
|
||||
"""Set the webhook URL (and optionally API key) used to email set-password links.
|
||||
|
||||
**Requires** the ``admin`` role.
|
||||
"""
|
||||
from app.services.password_setup_service import get_password_webhook_url, set_password_webhook_url
|
||||
from app.services.password_setup_service import (
|
||||
get_password_webhook_api_key,
|
||||
get_password_webhook_url,
|
||||
set_password_webhook_api_key,
|
||||
set_password_webhook_url,
|
||||
)
|
||||
|
||||
set_password_webhook_url(db, payload.url)
|
||||
if payload.api_key:
|
||||
set_password_webhook_api_key(db, payload.api_key)
|
||||
db.commit()
|
||||
url = get_password_webhook_url(db) or ""
|
||||
return PasswordWebhookConfigOut(configured=bool(url), url=url)
|
||||
api_key_set = bool(get_password_webhook_api_key(db))
|
||||
return PasswordWebhookConfigOut(configured=bool(url), url=url, api_key_set=api_key_set)
|
||||
|
||||
|
||||
@router.post("/jira-test")
|
||||
|
||||
@@ -28,6 +28,7 @@ from app.schemas.user import (
|
||||
UserPreferencesUpdate,
|
||||
UserOperatorOut,
|
||||
SendPasswordEmailOut,
|
||||
SwitchRoleRequest,
|
||||
)
|
||||
from app.services.audit_service import log_action
|
||||
|
||||
@@ -36,6 +37,7 @@ from app.services.user_service import (
|
||||
create_user_without_password,
|
||||
get_user_or_raise,
|
||||
list_users,
|
||||
switch_active_role,
|
||||
update_user,
|
||||
)
|
||||
from app.services.password_setup_service import send_password_setup_email
|
||||
@@ -85,6 +87,39 @@ def get_me(
|
||||
return current_user
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /users/me/switch-role — swap the caller's active role
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/me/switch-role", response_model=UserOut)
|
||||
def switch_my_role(
|
||||
payload: SwitchRoleRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> UserOut:
|
||||
"""Switch the caller's active role to one of their granted extra_roles.
|
||||
|
||||
Roles never mix — this changes which single role is active, it never
|
||||
grants permissions from more than one role at once. Takes effect
|
||||
immediately since every permission check reads ``role`` fresh from the
|
||||
DB on each request.
|
||||
"""
|
||||
with UnitOfWork(db) as uow:
|
||||
user = switch_active_role(db, current_user, payload.role)
|
||||
log_action(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
action="switch_role",
|
||||
entity_type="user",
|
||||
entity_id=current_user.id,
|
||||
details={"new_role": payload.role},
|
||||
)
|
||||
uow.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /users — list all users
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user