feat: add Firebase Auth with Google sign-in
Frontend guard shows login screen for unauthenticated users. Backend verifies Firebase ID tokens on all protected endpoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f0ef26ee15
commit
2a314af419
7 changed files with 62 additions and 6 deletions
|
|
@ -2,3 +2,12 @@ POSTGRES_USER=favs
|
|||
POSTGRES_PASSWORD=favs
|
||||
POSTGRES_DB=favs
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
|
||||
# Firebase
|
||||
VITE_FIREBASE_API_KEY=
|
||||
VITE_FIREBASE_AUTH_DOMAIN=
|
||||
VITE_FIREBASE_PROJECT_ID=
|
||||
VITE_FIREBASE_STORAGE_BUCKET=
|
||||
VITE_FIREBASE_MESSAGING_SENDER_ID=
|
||||
VITE_FIREBASE_APP_ID=
|
||||
VITE_FIREBASE_MEASUREMENT_ID=
|
||||
|
|
|
|||
24
backend/app/auth.py
Normal file
24
backend/app/auth.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import firebase_admin
|
||||
from firebase_admin import auth as firebase_auth, credentials
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from app.config import settings
|
||||
|
||||
if settings.firebase_project_id:
|
||||
firebase_admin.initialize_app(options={"projectId": settings.firebase_project_id})
|
||||
|
||||
bearer = HTTPBearer()
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
cred: HTTPAuthorizationCredentials = Depends(bearer),
|
||||
) -> dict:
|
||||
try:
|
||||
decoded = firebase_auth.verify_id_token(cred.credentials)
|
||||
return decoded
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid or expired token",
|
||||
)
|
||||
|
|
@ -5,6 +5,7 @@ class Settings(BaseSettings):
|
|||
database_url: str = "postgresql+asyncpg://favs:favs@favs-db:5432/favs"
|
||||
anthropic_api_key: str = ""
|
||||
categorize_model: str = "claude-haiku-4-5-20251001"
|
||||
firebase_project_id: str = ""
|
||||
|
||||
model_config = {"env_file": ".env"}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import get_current_user
|
||||
from app.database import get_db
|
||||
from app.models import Bookmark
|
||||
from app.schemas import BookmarkCreate, BookmarkResponse, BookmarkUpdate
|
||||
|
|
@ -13,7 +14,9 @@ router = APIRouter(prefix="/api/bookmarks", tags=["bookmarks"])
|
|||
|
||||
@router.get("/", response_model=list[BookmarkResponse])
|
||||
async def list_bookmarks(
|
||||
category: str | None = None, db: AsyncSession = Depends(get_db)
|
||||
category: str | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_user: dict = Depends(get_current_user),
|
||||
):
|
||||
query = select(Bookmark).order_by(Bookmark.created_at.desc())
|
||||
if category:
|
||||
|
|
@ -23,7 +26,7 @@ async def list_bookmarks(
|
|||
|
||||
|
||||
@router.get("/{bookmark_id}", response_model=BookmarkResponse)
|
||||
async def get_bookmark(bookmark_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
||||
async def get_bookmark(bookmark_id: uuid.UUID, db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user)):
|
||||
result = await db.execute(select(Bookmark).where(Bookmark.id == bookmark_id))
|
||||
bookmark = result.scalar_one_or_none()
|
||||
if not bookmark:
|
||||
|
|
@ -32,7 +35,7 @@ async def get_bookmark(bookmark_id: uuid.UUID, db: AsyncSession = Depends(get_db
|
|||
|
||||
|
||||
@router.post("/", response_model=BookmarkResponse, status_code=201)
|
||||
async def create_bookmark(data: BookmarkCreate, db: AsyncSession = Depends(get_db)):
|
||||
async def create_bookmark(data: BookmarkCreate, db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user)):
|
||||
bookmark = Bookmark(**data.model_dump())
|
||||
db.add(bookmark)
|
||||
await db.commit()
|
||||
|
|
@ -42,7 +45,7 @@ async def create_bookmark(data: BookmarkCreate, db: AsyncSession = Depends(get_d
|
|||
|
||||
@router.put("/{bookmark_id}", response_model=BookmarkResponse)
|
||||
async def update_bookmark(
|
||||
bookmark_id: uuid.UUID, data: BookmarkUpdate, db: AsyncSession = Depends(get_db)
|
||||
bookmark_id: uuid.UUID, data: BookmarkUpdate, db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user),
|
||||
):
|
||||
result = await db.execute(select(Bookmark).where(Bookmark.id == bookmark_id))
|
||||
bookmark = result.scalar_one_or_none()
|
||||
|
|
@ -56,7 +59,7 @@ async def update_bookmark(
|
|||
|
||||
|
||||
@router.delete("/{bookmark_id}", status_code=204)
|
||||
async def delete_bookmark(bookmark_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
||||
async def delete_bookmark(bookmark_id: uuid.UUID, db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user)):
|
||||
result = await db.execute(select(Bookmark).where(Bookmark.id == bookmark_id))
|
||||
bookmark = result.scalar_one_or_none()
|
||||
if not bookmark:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import get_current_user
|
||||
from app.categorizer import categorize_pending
|
||||
from app.database import get_db
|
||||
|
||||
|
|
@ -8,6 +9,6 @@ router = APIRouter(tags=["categorize"])
|
|||
|
||||
|
||||
@router.post("/api/categorize")
|
||||
async def run_categorize(db: AsyncSession = Depends(get_db)):
|
||||
async def run_categorize(db: AsyncSession = Depends(get_db), _user: dict = Depends(get_current_user)):
|
||||
count = await categorize_pending(db)
|
||||
return {"categorized": count}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ sqlalchemy[asyncio]==2.0.36
|
|||
asyncpg==0.30.0
|
||||
pydantic-settings==2.7.1
|
||||
anthropic==0.43.0
|
||||
firebase-admin==6.6.0
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ services:
|
|||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://favs:favs@favs-db:5432/favs
|
||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
|
||||
FIREBASE_PROJECT_ID: ${VITE_FIREBASE_PROJECT_ID}
|
||||
depends_on:
|
||||
favs-db:
|
||||
condition: service_healthy
|
||||
|
|
@ -29,5 +30,21 @@ services:
|
|||
- ./backend:/app
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
favs-ui:
|
||||
build:
|
||||
context: ./frontend
|
||||
args:
|
||||
- VITE_FIREBASE_API_KEY=${VITE_FIREBASE_API_KEY}
|
||||
- VITE_FIREBASE_AUTH_DOMAIN=${VITE_FIREBASE_AUTH_DOMAIN}
|
||||
- VITE_FIREBASE_PROJECT_ID=${VITE_FIREBASE_PROJECT_ID}
|
||||
- VITE_FIREBASE_STORAGE_BUCKET=${VITE_FIREBASE_STORAGE_BUCKET}
|
||||
- VITE_FIREBASE_MESSAGING_SENDER_ID=${VITE_FIREBASE_MESSAGING_SENDER_ID}
|
||||
- VITE_FIREBASE_APP_ID=${VITE_FIREBASE_APP_ID}
|
||||
- VITE_FIREBASE_MEASUREMENT_ID=${VITE_FIREBASE_MEASUREMENT_ID}
|
||||
ports:
|
||||
- "3000:80"
|
||||
depends_on:
|
||||
- favs-api
|
||||
|
||||
volumes:
|
||||
favs_pgdata:
|
||||
|
|
|
|||
Loading…
Reference in a new issue