1
0
card/api/app/routes/auth.py
2025-06-02 18:24:43 +09:00

133 lines
3.2 KiB
Python

"""Authentication routes"""
from datetime import timedelta
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.dependencies import (
AtprotoAuth,
create_access_token,
require_user,
AuthUser,
ACCESS_TOKEN_EXPIRE_MINUTES
)
from app.db.base import get_session
from app.repositories.user import UserRepository
# from app.services.atproto import AtprotoService
router = APIRouter(prefix="/auth", tags=["auth"])
class LoginRequest(BaseModel):
"""Login request model"""
identifier: str # Handle or DID
password: str # App password
class LoginResponse(BaseModel):
"""Login response model"""
access_token: str
token_type: str = "bearer"
did: str
handle: str
class VerifyResponse(BaseModel):
"""Verify response model"""
did: str
handle: str
valid: bool = True
@router.post("/login", response_model=LoginResponse)
async def login(
request: LoginRequest,
response: Response,
db: AsyncSession = Depends(get_session)
):
"""
Login with atproto credentials
- **identifier**: atproto handle or DID
- **password**: App password (not main password)
"""
auth = AtprotoAuth()
# Authenticate with atproto
user = await auth.authenticate(request.identifier, request.password)
if not user:
raise HTTPException(
status_code=401,
detail="Invalid credentials"
)
# Create or update user in database
user_repo = UserRepository(db)
await user_repo.get_or_create(did=user.did, handle=user.handle)
await db.commit()
# Create access token
access_token = create_access_token(
data={"did": user.did, "handle": user.handle},
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
)
# Set cookie for web clients
response.set_cookie(
key="ai_card_token",
value=access_token,
httponly=True,
secure=True,
samesite="lax",
max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60
)
return LoginResponse(
access_token=access_token,
did=user.did,
handle=user.handle or ""
)
@router.post("/logout")
async def logout(response: Response):
"""Logout and clear session"""
response.delete_cookie("ai_card_token")
return {"message": "Logged out successfully"}
@router.get("/verify", response_model=VerifyResponse)
async def verify_session(
current_user: AuthUser = Depends(require_user)
):
"""Verify current session is valid"""
return VerifyResponse(
did=current_user.did,
handle=current_user.handle or "",
valid=True
)
@router.post("/verify-did")
async def verify_did(did: str, handle: Optional[str] = None):
"""
Verify DID is valid (public endpoint)
- **did**: DID to verify
- **handle**: Optional handle to cross-check
"""
service = AtprotoService()
is_valid = await service.verify_did(did, handle)
return {
"did": did,
"handle": handle,
"valid": is_valid
}
# Import Optional here
from typing import Optional