File size: 5,325 Bytes
dc64ef1
37bcb58
ef8e0a7
b65231d
 
 
37bcb58
86a47ea
962a395
 
8c7a11b
962a395
8c7a11b
b65231d
dc64ef1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b65231d
350f1f4
 
ef8e0a7
 
350f1f4
 
8c7a11b
350f1f4
 
 
 
6a3fe66
350f1f4
 
 
 
 
 
 
 
f3c951d
350f1f4
b65231d
962a395
350f1f4
dc64ef1
350f1f4
ef8e0a7
6a3fe66
 
 
350f1f4
 
6a3fe66
 
 
37bcb58
ef8e0a7
 
 
 
 
 
 
37bcb58
 
 
f55b5b5
 
37bcb58
 
 
f55b5b5
37bcb58
 
f55b5b5
 
 
 
 
ef8e0a7
 
6a3fe66
dc64ef1
 
34d7bbf
 
6a3fe66
350f1f4
 
 
dc64ef1
350f1f4
 
 
 
 
 
 
ef8e0a7
 
 
 
 
 
 
 
 
 
 
 
 
37bcb58
f55b5b5
 
37bcb58
 
 
f55b5b5
37bcb58
 
f55b5b5
 
 
 
 
37bcb58
ef8e0a7
 
 
 
 
 
 
350f1f4
 
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import time
import jwt as pyjwt
from fastapi import HTTPException, status, Request
from fastapi.security import OAuth2PasswordBearer
from typing import Any, Optional

from src.config import settings
from src.database import get_supabase, get_auth_supabase

DEV_USER_ID = "00000000-0000-0000-0000-000000000001"
DEV_USER = {"id": DEV_USER_ID, "email": "dev@studymate.ai", "name": "Dev User"}

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)

_TOKEN_CACHE: dict = {}
_TOKEN_CACHE_TTL = 300  # 5 minutes


def _verify_token_cached(client, token: str) -> Any:
    now = time.time()

    # Simple cleanup to prevent unbounded growth
    if len(_TOKEN_CACHE) > 1000:
        expired = [k for k, v in _TOKEN_CACHE.items() if now - v[1] > _TOKEN_CACHE_TTL]
        for k in expired:
            del _TOKEN_CACHE[k]

    # Return cached user if valid
    if token in _TOKEN_CACHE:
        user, timestamp = _TOKEN_CACHE[token]
        if now - timestamp < _TOKEN_CACHE_TTL:
            return user

    # Not cached or expired β€” fetch from Supabase
    response = client.auth.get_user(token)
    user = getattr(response, "user", None) or response
    if user:
        _TOKEN_CACHE[token] = (user, now)
        return user

    raise ValueError("Invalid token response")


def _extract_token(request: Request) -> Optional[str]:
    """
    Extract JWT from headers β€” case-insensitive.
    Priority: X-Auth-Token β†’ Authorization (skip HF space tokens)
    """
    headers = {k.lower(): v for k, v in request.headers.items()}

    # 1. Try X-Auth-Token first (our custom header)
    token = headers.get("x-auth-token")
    if token:
        return token

    # 2. Fallback to Authorization header
    auth = headers.get("authorization", "")
    if auth.startswith("Bearer "):
        token = auth[len("Bearer "):].strip()
        # Skip HF tokens β€” they're for space access, not user auth
        if token.startswith("hf_"):
            return None
        return token

    return None


async def get_current_user_id(request: Request) -> str:
    client = get_auth_supabase() or get_supabase()

    # Dev mode β€” no Supabase configured
    if client is None:
        return DEV_USER_ID

    token = _extract_token(request)

    if not token:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated")

    # ── Mode 1: our own stateless JWT ────────────────────────────────────────
    try:
        from src.auth.jwt_utils import decode_access_token
        payload = decode_access_token(token)
        user_id = payload.get("sub")
        if user_id:
            return str(user_id)
    except Exception:
        pass

    # ── Mode 1b: Supabase JWT stateless fallback (avoids 403 network race) ───
    if settings.supabase_jwt_secret:
        try:
            payload = pyjwt.decode(
                token,
                settings.supabase_jwt_secret,
                algorithms=["HS256", "HS384", "HS512"],
                options={"verify_aud": False},
            )
            user_id = payload.get("sub")
            if user_id:
                return str(user_id)
        except Exception:
            pass

    # ── Mode 2: Supabase token (backwards-compat for Google OAuth sessions) ──
    try:
        user = _verify_token_cached(client, token)
        return str(user.id)
    except Exception as e:
        print(f"Token validation error: {e}")
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid or expired token")


async def get_current_user(request: Request) -> Any:
    client = get_auth_supabase() or get_supabase()

    # Dev mode
    if client is None:
        return DEV_USER

    token = _extract_token(request)

    if not token:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated")

    # ── Mode 1: our own stateless JWT ────────────────────────────────────────
    try:
        from src.auth.jwt_utils import decode_access_token
        payload = decode_access_token(token)
        user_id = payload.get("sub")
        if user_id:
            return {"id": user_id, "email": payload.get("email", "")}
    except Exception:
        pass

    # ── Mode 1b: Supabase JWT stateless fallback ─────────────────────────────
    if settings.supabase_jwt_secret:
        try:
            payload = pyjwt.decode(
                token,
                settings.supabase_jwt_secret,
                algorithms=["HS256", "HS384", "HS512"],
                options={"verify_aud": False},
            )
            user_id = payload.get("sub")
            if user_id:
                return {"id": str(user_id), "email": payload.get("email", "")}
        except Exception:
            pass

    # ── Mode 2: Supabase token (backwards compat) ────────────────────────────
    try:
        user = _verify_token_cached(client, token)
        if user:
            return user
    except Exception as e:
        print(f"Token validation error: {e}")

    raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated")