feat(04-rbac-01): RBAC guards + invite token + email + invitation API
- rbac.py: PortalCaller dataclass + get_portal_caller dependency (header-based)
- rbac.py: require_platform_admin (403 for non-platform_admin)
- rbac.py: require_tenant_admin (platform_admin bypasses; customer_admin
checks UserTenantRole; operator always rejected)
- rbac.py: require_tenant_member (platform_admin bypasses; all roles
checked against UserTenantRole)
- invite_token.py: generate_invite_token (HMAC-SHA256, base64url, 48h TTL)
- invite_token.py: validate_invite_token (timing-safe compare_digest, TTL check)
- invite_token.py: token_to_hash (SHA-256 for DB storage)
- email.py: send_invite_email (sync smtplib, skips if smtp_host empty)
- invitations.py: POST /api/portal/invitations (create, requires tenant admin)
- invitations.py: POST /api/portal/invitations/accept (accept invitation)
- invitations.py: POST /api/portal/invitations/{id}/resend (regenerate token)
- invitations.py: GET /api/portal/invitations (list pending)
- portal.py: AuthVerifyResponse now returns role+tenant_ids+active_tenant_id
- portal.py: auth/register gated behind require_platform_admin
- tasks.py: send_invite_email_task Celery task (fire-and-forget)
- gateway/main.py: invitations_router mounted
This commit is contained in:
@@ -170,6 +170,36 @@ async def _embed_and_store_async(
|
||||
current_tenant_id.reset(token)
|
||||
|
||||
|
||||
@app.task(
|
||||
name="orchestrator.tasks.send_invite_email_task",
|
||||
bind=False,
|
||||
max_retries=2,
|
||||
default_retry_delay=30,
|
||||
ignore_result=True, # Fire-and-forget — callers don't await the result
|
||||
)
|
||||
def send_invite_email_task(
|
||||
to_email: str,
|
||||
invitee_name: str,
|
||||
tenant_name: str,
|
||||
invite_url: str,
|
||||
) -> None:
|
||||
"""
|
||||
Asynchronously send an invitation email via SMTP.
|
||||
|
||||
Dispatched fire-and-forget by the invitation API after creating an invitation.
|
||||
If SMTP is not configured, logs a warning and returns silently.
|
||||
|
||||
Args:
|
||||
to_email: Recipient email address.
|
||||
invitee_name: Recipient display name.
|
||||
tenant_name: Name of the tenant being joined.
|
||||
invite_url: Full invitation acceptance URL.
|
||||
"""
|
||||
from shared.email import send_invite_email
|
||||
|
||||
send_invite_email(to_email, invitee_name, tenant_name, invite_url)
|
||||
|
||||
|
||||
@app.task(
|
||||
name="orchestrator.tasks.handle_message",
|
||||
bind=True,
|
||||
|
||||
Reference in New Issue
Block a user