1
0

add claude

This commit is contained in:
2025-06-01 21:39:53 +09:00
parent 3459231bba
commit 4246f718ef
80 changed files with 7249 additions and 0 deletions

View File

@ -0,0 +1 @@
# Routes Package

132
api/app/routes/auth.py Normal file
View File

@ -0,0 +1,132 @@
"""Authentication routes"""
from datetime import timedelta
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_db
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_db)
):
"""
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

118
api/app/routes/cards.py Normal file
View File

@ -0,0 +1,118 @@
"""Card-related API routes"""
from typing import List
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.card import Card, CardDraw, CardDrawResult
from app.services.gacha import GachaService
from app.repositories.user import UserRepository
from app.repositories.card import CardRepository, UniqueCardRepository
from app.db.base import get_db
router = APIRouter(prefix="/cards", tags=["cards"])
@router.post("/draw", response_model=CardDrawResult)
async def draw_card(
draw_request: CardDraw,
db: AsyncSession = Depends(get_db)
):
"""
カードを抽選する
- **user_did**: ユーザーのatproto DID
- **is_paid**: 課金ガチャかどうか
"""
try:
gacha_service = GachaService(db)
card, is_unique = await gacha_service.draw_card(
user_did=draw_request.user_did,
is_paid=draw_request.is_paid
)
# 演出タイプを決定
animation_type = "normal"
if is_unique:
animation_type = "unique"
elif card.status.value == "kira":
animation_type = "kira"
elif card.status.value in ["super_rare", "rare"]:
animation_type = "rare"
# 新規取得かチェック
user_repo = UserRepository(db)
card_repo = CardRepository(db)
user = await user_repo.get_by_did(draw_request.user_did)
count = await card_repo.count_user_cards(user.id, card.id)
is_new = count == 1 # 今引いたカードが初めてなら1枚
result = CardDrawResult(
card=card,
is_new=is_new,
animation_type=animation_type
)
await db.commit()
return result
except Exception as e:
await db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/user/{user_did}", response_model=List[Card])
async def get_user_cards(
user_did: str,
skip: int = 0,
limit: int = 100,
db: AsyncSession = Depends(get_db)
):
"""
ユーザーの所有カード一覧を取得
- **user_did**: ユーザーのatproto DID
"""
user_repo = UserRepository(db)
card_repo = CardRepository(db)
user = await user_repo.get_by_did(user_did)
if not user:
raise HTTPException(status_code=404, detail="User not found")
user_cards = await card_repo.get_user_cards(user.id, skip=skip, limit=limit)
# Convert to API model
cards = []
for uc in user_cards:
card = Card(
id=uc.card_id,
cp=uc.cp,
status=uc.status,
skill=uc.skill,
owner_did=user_did,
obtained_at=uc.obtained_at,
is_unique=uc.is_unique,
unique_id=str(uc.unique_id) if uc.unique_id else None
)
cards.append(card)
return cards
@router.get("/unique")
async def get_unique_cards(db: AsyncSession = Depends(get_db)):
"""
全てのuniqueカード一覧を取得所有者情報付き
"""
unique_repo = UniqueCardRepository(db)
unique_cards = await unique_repo.get_all_unique_cards()
return [
{
"card_id": uc.card_id,
"owner_did": uc.owner_did,
"obtained_at": uc.obtained_at,
"unique_id": str(uc.unique_id)
}
for uc in unique_cards
]

151
api/app/routes/sync.py Normal file
View File

@ -0,0 +1,151 @@
"""Synchronization routes for atproto"""
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from app.auth.dependencies import require_user, AuthUser
from app.db.base import get_db
from app.services.card_sync import CardSyncService
from app.repositories.user import UserRepository
router = APIRouter(prefix="/sync", tags=["sync"])
class SyncRequest(BaseModel):
"""Sync request model"""
atproto_session: str # Session string from atproto login
class SyncResponse(BaseModel):
"""Sync response model"""
synced_to_pds: int = 0
imported_from_pds: int = 0
message: str
@router.post("/cards", response_model=SyncResponse)
async def sync_cards(
request: SyncRequest,
current_user: AuthUser = Depends(require_user),
db: AsyncSession = Depends(get_db)
):
"""
Sync cards between database and atproto PDS
- **atproto_session**: Session string from atproto login
"""
try:
# Get user from database
user_repo = UserRepository(db)
user = await user_repo.get_by_did(current_user.did)
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Create sync service
sync_service = CardSyncService(db, request.atproto_session)
# Import from PDS first
imported = await sync_service.import_cards_from_pds(current_user.did)
# Then sync all cards to PDS
synced = await sync_service.sync_all_user_cards(user.id, current_user.did)
await db.commit()
return SyncResponse(
synced_to_pds=synced,
imported_from_pds=imported,
message=f"Successfully synced {synced} cards to PDS and imported {imported} cards from PDS"
)
except Exception as e:
await db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.post("/export")
async def export_to_pds(
request: SyncRequest,
current_user: AuthUser = Depends(require_user),
db: AsyncSession = Depends(get_db)
):
"""
Export all cards to atproto PDS
- **atproto_session**: Session string from atproto login
"""
try:
user_repo = UserRepository(db)
user = await user_repo.get_by_did(current_user.did)
if not user:
raise HTTPException(status_code=404, detail="User not found")
sync_service = CardSyncService(db, request.atproto_session)
synced = await sync_service.sync_all_user_cards(user.id, current_user.did)
return {
"exported": synced,
"message": f"Successfully exported {synced} cards to PDS"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/import")
async def import_from_pds(
request: SyncRequest,
current_user: AuthUser = Depends(require_user),
db: AsyncSession = Depends(get_db)
):
"""
Import cards from atproto PDS
- **atproto_session**: Session string from atproto login
"""
try:
sync_service = CardSyncService(db, request.atproto_session)
imported = await sync_service.import_cards_from_pds(current_user.did)
await db.commit()
return {
"imported": imported,
"message": f"Successfully imported {imported} cards from PDS"
}
except Exception as e:
await db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/verify/{card_id}")
async def verify_card_ownership(
card_id: int,
unique_id: Optional[str] = None,
current_user: AuthUser = Depends(require_user),
db: AsyncSession = Depends(get_db)
):
"""
Verify user owns a specific card
- **card_id**: Card type ID (0-15)
- **unique_id**: Unique ID for unique cards
"""
sync_service = CardSyncService(db)
owns_card = await sync_service.verify_card_ownership(
current_user.did,
card_id,
unique_id
)
return {
"card_id": card_id,
"unique_id": unique_id,
"owned": owns_card
}
# Import Optional
from typing import Optional