File size: 3,116 Bytes
45dc2a1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
"""
Real Firebase ID token verification — decoupled from LOCAL_MODE (which only governs
storage) and from REAL_SEND_ENABLED (mail transport), same independent-flag pattern as
backend/mailer_client.py. A request either carries a real Firebase ID token
(REAL_AUTH_ENABLED=true) or, for local/dev/test convenience, an X-Dev-Uid header
(REAL_AUTH_ENABLED=false — the default in tests, see tests/conftest.py).
"""

from __future__ import annotations

import os

import firebase_admin
from fastapi import Header, HTTPException
from firebase_admin import auth as firebase_auth

from backend.firebase_app import get_app


def real_auth_enabled() -> bool:
    return os.getenv("REAL_AUTH_ENABLED", "false").lower() == "true"


class AuthError(Exception):
    pass


def verify_id_token(token: str) -> dict:
    get_app()
    try:
        return firebase_auth.verify_id_token(token)
    except firebase_admin.exceptions.FirebaseError as exc:
        raise AuthError(str(exc)) from exc


def current_uid(
    authorization: str | None = Header(default=None),
    x_dev_uid: str = Header(default="dev-user-1", alias="X-Dev-Uid"),
) -> str:
    """FastAPI dependency — every route that needs "who is calling" uses this instead of
    reading X-Dev-Uid directly, so the real/local branch lives in exactly one place."""
    if not real_auth_enabled():
        return x_dev_uid

    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Missing bearer token")

    token = authorization[len("Bearer "):]
    try:
        claims = verify_id_token(token)
    except AuthError as exc:
        raise HTTPException(status_code=401, detail=f"Invalid token: {exc}") from exc
    # Password sign-ups must prove they own the address before touching anything real.
    if not claims.get("email_verified", False):
        raise HTTPException(status_code=403, detail="Email not verified")
    return claims["uid"]


def current_email(
    authorization: str | None = Header(default=None),
    x_dev_email: str = Header(default="", alias="X-Dev-Email"),
) -> str:
    """The signed-in address (checked to be verified by current_uid). Used where an
    action depends on who the person is, such as accepting an invite."""
    if not real_auth_enabled():
        return x_dev_email
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Missing bearer token")
    try:
        claims = verify_id_token(authorization[len("Bearer "):])
    except AuthError as exc:
        raise HTTPException(status_code=401, detail=f"Invalid token: {exc}") from exc
    return claims.get("email", "")


def lookup_user(uid: str) -> dict:
    """{email, name} for a uid, best effort; empty when unknown or running locally."""
    if not real_auth_enabled():
        return {}
    try:
        get_app()
        record = firebase_auth.get_user(uid)
    except Exception:  # noqa: BLE001 — a missing name must not break the team list
        return {}
    return {"email": record.email or "", "name": record.display_name or ""}