app.bettersight.io/backend/services/auth_service.py

61 lines
2.3 KiB
Python

from repositories.pocketbase_client import pb_client
from repositories.tenant_repository import tenant_repository
from repositories.seat_repository import seat_repository
def get_seat_from_request(request):
"""
Resolves both the tenant_id AND the specific active seat record for a
dashboard request from its `Authorization: Bearer {jwt}` header. The
JWT itself is issued and verified by PocketBase (the dashboard
authenticates directly against PocketBase) — this backend doesn't
hold the JWT signing secret, so it validates the token by asking
PocketBase to refresh it, then resolves the returned email to a
tenant and seat.
Needed by seat-scoped features (the trip watchlist's per-seat cap)
where "which tenant" isn't enough — every other authenticated route
only ever needed tenant_id, so the seat record this lookup already
finds was previously discarded (see get_tenant_id_from_request()
below, now a thin wrapper around this).
Data flow:
request.headers['Authorization'] → strip "Bearer " prefix →
PocketBase POST /api/collections/users/auth-refresh →
{ record: { email } } → seat lookup by email across known tenants
(via domain) → (tenant_id, seat) returned, or (None, None) if
unresolvable
"""
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return None, None
token = auth_header[len('Bearer '):]
record = pb_client.auth_refresh(token)
if not record or not record.get('email'):
return None, None
email = record['email']
domain = email.split('@', 1)[1].lower() if '@' in email else None
if not domain:
return None, None
tenant = tenant_repository.get_by_domain(domain)
if not tenant:
return None, None
# Confirm this email actually holds an active seat on the tenant —
# a verified PocketBase login alone isn't sufficient authorization,
# the email must also be a registered, active seat.
seat = seat_repository.get_active(tenant['id'], email)
if not seat:
return None, None
return tenant['id'], seat
def get_tenant_id_from_request(request):
"""Back-compat wrapper — most routes only need the tenant_id, not the seat."""
tenant_id, _ = get_seat_from_request(request)
return tenant_id