133 lines
3.2 KiB
Python
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 |