Expand portal onboarding and administration
This commit is contained in:
parent
3d334479e9
commit
8229c6dd33
17 changed files with 1601 additions and 32 deletions
|
|
@ -7,6 +7,7 @@ from user_engine.adapters.local import (
|
|||
from user_engine.adapters.postgres import PostgresUserEngineStore
|
||||
from user_engine.adapters.claims import VerifiedIdentityClaimsAdapter
|
||||
from user_engine.adapters.provisioning import HTTPIdentityProvisioningAdapter
|
||||
from user_engine.adapters.tenant_management import HTTPTenantManagementAdapter
|
||||
|
||||
__all__ = [
|
||||
"InMemoryUserEngineStore",
|
||||
|
|
@ -14,4 +15,5 @@ __all__ = [
|
|||
"PostgresUserEngineStore",
|
||||
"VerifiedIdentityClaimsAdapter",
|
||||
"HTTPIdentityProvisioningAdapter",
|
||||
"HTTPTenantManagementAdapter",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -161,6 +161,12 @@ class InMemoryUserEngineStore:
|
|||
if invitation.user_id == user_id
|
||||
)
|
||||
|
||||
def family_invitations_for_tenant(self, tenant: str) -> tuple[FamilyInvitation, ...]:
|
||||
return tuple(
|
||||
invitation for invitation in self.family_invitations.values()
|
||||
if invitation.tenant == tenant
|
||||
)
|
||||
|
||||
def save_registration_session(self, session: RegistrationSession) -> None:
|
||||
self.registration_sessions[session.registration_id] = session
|
||||
|
||||
|
|
@ -324,7 +330,19 @@ class InMemoryUserEngineStore:
|
|||
self.outbox_events.append(event)
|
||||
|
||||
def pending_outbox(self) -> tuple[OutboxEvent, ...]:
|
||||
return tuple(self.outbox_events)
|
||||
return tuple(
|
||||
item for item in self.outbox_events
|
||||
if item.delivered_at is None and item.dead_lettered_at is None
|
||||
)
|
||||
|
||||
def save_outbox(self, event: OutboxEvent) -> None:
|
||||
self.outbox_events = [
|
||||
event if item.event_id == event.event_id else item
|
||||
for item in self.outbox_events
|
||||
]
|
||||
|
||||
def outbox_event(self, event_id: str) -> OutboxEvent | None:
|
||||
return next((item for item in self.outbox_events if item.event_id == event_id), None)
|
||||
|
||||
def record_counts(self) -> Mapping[str, int]:
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -198,6 +198,12 @@ class PostgresUserEngineStore:
|
|||
self._query_records("family_invitations", user_id=user_id),
|
||||
)
|
||||
|
||||
def family_invitations_for_tenant(self, tenant: str) -> tuple[FamilyInvitation, ...]:
|
||||
return cast(
|
||||
tuple[FamilyInvitation, ...],
|
||||
self._query_records("family_invitations", tenant=tenant),
|
||||
)
|
||||
|
||||
def save_registration_session(self, session: RegistrationSession) -> None:
|
||||
self._upsert_record(session)
|
||||
|
||||
|
|
@ -413,7 +419,7 @@ class PostgresUserEngineStore:
|
|||
"""
|
||||
SELECT payload
|
||||
FROM user_engine_outbox_events
|
||||
WHERE claimed_at IS NULL AND delivered_at IS NULL
|
||||
WHERE claimed_at IS NULL AND delivered_at IS NULL AND failed_at IS NULL
|
||||
ORDER BY occurred_at, event_id
|
||||
"""
|
||||
)
|
||||
|
|
@ -422,6 +428,31 @@ class PostgresUserEngineStore:
|
|||
for row in cursor.fetchall()
|
||||
)
|
||||
|
||||
def save_outbox(self, event: OutboxEvent) -> None:
|
||||
store_record = store_record_for(event)
|
||||
with self._cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE user_engine_outbox_events
|
||||
SET payload = %s::jsonb, claimed_at = %s, claimed_by = %s,
|
||||
delivered_at = %s, failed_at = %s, failure_reason = %s
|
||||
WHERE event_id = %s
|
||||
""",
|
||||
(json.dumps(store_record.payload), event.claimed_at, event.claimed_by,
|
||||
event.delivered_at, event.failed_at, event.failure_reason, event.event_id),
|
||||
)
|
||||
|
||||
def outbox_event(self, event_id: str) -> OutboxEvent | None:
|
||||
with self._cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT payload FROM user_engine_outbox_events WHERE event_id = %s",
|
||||
(event_id,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
return None if row is None else cast(
|
||||
OutboxEvent, self._decode_payload_row("outbox_events", row)
|
||||
)
|
||||
|
||||
def record_counts(self) -> Mapping[str, int]:
|
||||
counts = {key: 0 for key in USER_ENGINE_RECORD_COUNT_KEYS}
|
||||
with self._cursor() as cursor:
|
||||
|
|
|
|||
52
src/user_engine/adapters/tenant_management.py
Normal file
52
src/user_engine/adapters/tenant_management.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""HTTP adapter for the provider-neutral tenant authority."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from user_engine.ports import TenantProvisioningResult
|
||||
|
||||
|
||||
class HTTPTenantManagementAdapter:
|
||||
def __init__(self, *, base_url: str, bearer_token: str, timeout: float = 10) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.bearer_token = bearer_token.strip()
|
||||
if not self.bearer_token:
|
||||
raise ValueError("bearer token must not be empty")
|
||||
self.timeout = timeout
|
||||
|
||||
def create_tenant(
|
||||
self, *, tenant: str, display_name: str, idempotency_key: str,
|
||||
correlation_id: str,
|
||||
) -> TenantProvisioningResult:
|
||||
request = Request(
|
||||
self.base_url + "/v1/tenants",
|
||||
data=json.dumps({
|
||||
"tenant": tenant, "display_name": display_name,
|
||||
"idempotency_key": idempotency_key,
|
||||
"correlation_id": correlation_id,
|
||||
}).encode(),
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.bearer_token}",
|
||||
"Content-Type": "application/json",
|
||||
"Idempotency-Key": idempotency_key,
|
||||
"X-Request-ID": correlation_id,
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urlopen(request, timeout=self.timeout) as response:
|
||||
result = json.loads(response.read())
|
||||
except HTTPError as exc:
|
||||
exc.read(4096)
|
||||
raise RuntimeError(f"tenant authority failed ({exc.code})") from exc
|
||||
except URLError as exc:
|
||||
raise RuntimeError("tenant authority unavailable") from exc
|
||||
return TenantProvisioningResult(
|
||||
tenant=str(result.get("tenant") or tenant),
|
||||
status=str(result["status"]),
|
||||
resumed=bool(result.get("resumed", False)),
|
||||
external_ref=str(result["external_ref"]) if result.get("external_ref") else None,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue