Vatxzz commited on
Commit
dfd5a64
·
1 Parent(s): 0c13fc3

fixed thumbnails

Browse files
app/lib/data/services/api_client.dart CHANGED
@@ -5,6 +5,7 @@ library;
5
  import 'dart:async';
6
  import 'dart:convert';
7
 
 
8
  import 'package:http/http.dart' as http;
9
 
10
  import '../../domain/models/artifact.dart';
@@ -202,7 +203,14 @@ class ApiClient {
202
  String resolveMedia(String ref) {
203
  if (ref.startsWith('http://') || ref.startsWith('https://')) return ref;
204
  final path = ref.startsWith('/') ? ref : '/$ref';
205
- return '$baseUrl$path';
 
 
 
 
 
 
 
206
  }
207
 
208
  // ------------------------------------------------------------------------- //
 
5
  import 'dart:async';
6
  import 'dart:convert';
7
 
8
+ import 'package:flutter/foundation.dart' show kIsWeb;
9
  import 'package:http/http.dart' as http;
10
 
11
  import '../../domain/models/artifact.dart';
 
203
  String resolveMedia(String ref) {
204
  if (ref.startsWith('http://') || ref.startsWith('https://')) return ref;
205
  final path = ref.startsWith('/') ? ref : '/$ref';
206
+ final url = '$baseUrl$path';
207
+ // Web <img> tags can't send an Authorization header, so the auth-gated
208
+ // /media proxy is reached with the token as a query param instead. (Mobile
209
+ // uses mediaHeaders.) The token is URL-safe (base64url JWT); no encoding.
210
+ if (kIsWeb && path.startsWith('/media/') && _lastToken != null) {
211
+ return '$url?token=$_lastToken';
212
+ }
213
+ return url;
214
  }
215
 
216
  // ------------------------------------------------------------------------- //
app/lib/data/services/auth_service.dart CHANGED
@@ -42,10 +42,12 @@ abstract class AuthService {
42
  class FirebaseAuthService implements AuthService {
43
  FirebaseAuthService({fb.FirebaseAuth? auth, GoogleSignIn? google})
44
  : _auth = auth ?? fb.FirebaseAuth.instance,
45
- _google = google ?? GoogleSignIn();
 
 
46
 
47
  final fb.FirebaseAuth _auth;
48
- final GoogleSignIn _google;
49
 
50
  AuthUser? _map(fb.User? u) => u == null
51
  ? null
@@ -77,7 +79,7 @@ class FirebaseAuthService implements AuthService {
77
  // (throws UnimplementedError); Firebase's popup flow handles Google there.
78
  if (kIsWeb) return _signInWithGoogleWeb(mergeGuestData: mergeGuestData);
79
 
80
- final account = await _google.signIn();
81
  if (account == null) throw fb.FirebaseAuthException(code: 'canceled');
82
  final gAuth = await account.authentication;
83
  final credential = fb.GoogleAuthProvider.credential(
@@ -150,7 +152,7 @@ class FirebaseAuthService implements AuthService {
150
 
151
  @override
152
  Future<void> signOut() async {
153
- await _google.signOut();
154
  await _auth.signOut();
155
  }
156
  }
 
42
  class FirebaseAuthService implements AuthService {
43
  FirebaseAuthService({fb.FirebaseAuth? auth, GoogleSignIn? google})
44
  : _auth = auth ?? fb.FirebaseAuth.instance,
45
+ // On web, `google_sign_in` asserts a client-id at construction and web
46
+ // sign-in goes through Firebase's popup instead — so never build it there.
47
+ _google = google ?? (kIsWeb ? null : GoogleSignIn());
48
 
49
  final fb.FirebaseAuth _auth;
50
+ final GoogleSignIn? _google;
51
 
52
  AuthUser? _map(fb.User? u) => u == null
53
  ? null
 
79
  // (throws UnimplementedError); Firebase's popup flow handles Google there.
80
  if (kIsWeb) return _signInWithGoogleWeb(mergeGuestData: mergeGuestData);
81
 
82
+ final account = await _google!.signIn(); // non-null: web returns above
83
  if (account == null) throw fb.FirebaseAuthException(code: 'canceled');
84
  final gAuth = await account.authentication;
85
  final credential = fb.GoogleAuthProvider.credential(
 
152
 
153
  @override
154
  Future<void> signOut() async {
155
+ await _google?.signOut();
156
  await _auth.signOut();
157
  }
158
  }
backend/app/api/media.py CHANGED
@@ -15,7 +15,7 @@ from typing import Any
15
  from fastapi import APIRouter, HTTPException
16
  from fastapi.responses import FileResponse
17
 
18
- from app.auth import OwnerDep
19
  from app.config import get_settings
20
  from app.store import db
21
 
@@ -25,7 +25,7 @@ router = APIRouter(prefix="/media", tags=["media"])
25
 
26
 
27
  @router.get("/{card_id}/{filename}")
28
- async def get_media(card_id: str, filename: str, owner_id: OwnerDep) -> Any:
29
  """Stream one media file for a card the caller owns.
30
 
31
  404 for unknown or non-owned cards (never reveal another owner's media);
 
15
  from fastapi import APIRouter, HTTPException
16
  from fastapi.responses import FileResponse
17
 
18
+ from app.auth import MediaOwnerDep
19
  from app.config import get_settings
20
  from app.store import db
21
 
 
25
 
26
 
27
  @router.get("/{card_id}/{filename}")
28
+ async def get_media(card_id: str, filename: str, owner_id: MediaOwnerDep) -> Any:
29
  """Stream one media file for a card the caller owns.
30
 
31
  404 for unknown or non-owned cards (never reveal another owner's media);
backend/app/auth.py CHANGED
@@ -13,7 +13,7 @@ import asyncio
13
  import logging
14
  from typing import Annotated
15
 
16
- from fastapi import Depends, Header, HTTPException
17
  from google.auth.transport import requests as google_requests
18
  from google.oauth2 import id_token as google_id_token
19
 
@@ -68,3 +68,34 @@ async def get_owner(authorization: str | None = Header(None)) -> str:
68
 
69
 
70
  OwnerDep = Annotated[str, Depends(get_owner)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  import logging
14
  from typing import Annotated
15
 
16
+ from fastapi import Depends, Header, HTTPException, Query
17
  from google.auth.transport import requests as google_requests
18
  from google.oauth2 import id_token as google_id_token
19
 
 
68
 
69
 
70
  OwnerDep = Annotated[str, Depends(get_owner)]
71
+
72
+
73
+ async def get_owner_query_or_header(
74
+ authorization: str | None = Header(None),
75
+ token: str | None = Query(None),
76
+ ) -> str:
77
+ """Like [get_owner] but also accepts the token as a `?token=` query param.
78
+
79
+ Browser `<img>` tags can't send an Authorization header, so web thumbnails
80
+ fetch the auth-gated /media proxy with the token in the query string."""
81
+ if not get_settings().firebase_project_id:
82
+ raise HTTPException(status_code=503, detail="auth not configured")
83
+ raw: str | None = None
84
+ if authorization and authorization.startswith("Bearer "):
85
+ raw = authorization.removeprefix("Bearer ").strip()
86
+ elif token:
87
+ raw = token.strip()
88
+ if not raw:
89
+ raise HTTPException(status_code=401, detail="missing bearer token")
90
+ try:
91
+ decoded = await verify_async(raw)
92
+ except Exception as exc:
93
+ log.info("token verification failed: %s: %s", type(exc).__name__, exc)
94
+ raise HTTPException(status_code=401, detail="invalid or expired token")
95
+ uid = uid_of(decoded)
96
+ if not uid:
97
+ raise HTTPException(status_code=401, detail="token missing subject")
98
+ return uid
99
+
100
+
101
+ MediaOwnerDep = Annotated[str, Depends(get_owner_query_or_header)]