1707 lines
54 KiB
Python
1707 lines
54 KiB
Python
"""
|
|
User data API endpoints router.
|
|
|
|
Provides CRUD operations for user sessions, decks, replays,
|
|
card collection, groups, networks, preferences, and activity logs.
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, update, delete
|
|
from typing import Optional, List
|
|
from datetime import datetime, timezone
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import get_current_user
|
|
from app.models.models import User
|
|
from app.models.user_data import (
|
|
UserSession, DeckVersion, GameReplay, ReplayPlayer,
|
|
GameOutcome, UserStatistics, UserCardCollection, CardWishlist,
|
|
UserGroup, GroupMember, GroupChatMessage, UserNetwork,
|
|
NetworkMember, UserPreference, UserActivityLog
|
|
)
|
|
from app.schemas.user_data_schemas import (
|
|
SessionCleanupResponse,
|
|
DeckVersionCreate, DeckVersionUpdate, DeckVersionResponse, DeckVersionListResponse,
|
|
GameReplayCreate, GameReplayUpdate, GameReplayResponse, GameReplayListResponse,
|
|
GameOutcomeCreate, GameOutcomeResponse, GameOutcomeListResponse,
|
|
UserStatisticsResponse, StatisticsUpdateResponse,
|
|
CardCollectionCreate, CardCollectionUpdate, CardCollectionResponse, CardCollectionListResponse,
|
|
WishlistCreate, WishlistUpdate, WishlistResponse, WishlistListResponse,
|
|
GroupCreate, GroupUpdate, GroupResponse, GroupListResponse,
|
|
GroupMemberCreate, GroupMemberUpdate, GroupMemberRemove,
|
|
GroupChatMessageCreate, GroupChatMessageResponse, GroupChatMessageListResponse,
|
|
NetworkCreate, NetworkUpdate, NetworkResponse, NetworkListResponse,
|
|
NetworkMemberCreate,
|
|
UserPreferenceUpdate, UserPreferenceResponse,
|
|
ActivityLogEntry, ActivityLogListResponse,
|
|
MessageResponse, CountResponse, ErrorDetail
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# ===== Session Management =====
|
|
|
|
@router.get("/sessions/me", response_model=List[SessionCleanupResponse])
|
|
async def get_current_sessions(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get active sessions for current user."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = (
|
|
select(UserSession)
|
|
.where(
|
|
UserSession.user_id == user_id,
|
|
UserSession.is_active == True,
|
|
UserSession.expires_at > datetime.now(timezone.utc)
|
|
)
|
|
)
|
|
result = await db.execute(stmt)
|
|
sessions = result.scalars().all()
|
|
|
|
return [
|
|
SessionCleanupResponse(
|
|
cleaned_count=len(sessions),
|
|
message=f"Found {len(sessions)} active sessions"
|
|
)
|
|
]
|
|
|
|
|
|
@router.delete("/sessions/cleanup", response_model=MessageResponse)
|
|
async def cleanup_expired_sessions(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Cleanup expired sessions for current user."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
stmt = (
|
|
update(UserSession)
|
|
.where(
|
|
UserSession.user_id == user_id,
|
|
UserSession.is_active == True,
|
|
UserSession.expires_at < now
|
|
)
|
|
.values(is_active=False)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
# Count cleaned sessions
|
|
count_stmt = select(func.count()).select_from(UserSession).where(
|
|
UserSession.user_id == user_id,
|
|
UserSession.is_active == False,
|
|
UserSession.expires_at < now
|
|
)
|
|
count_result = await db.execute(count_stmt)
|
|
cleaned_count = count_result.scalar()
|
|
|
|
return MessageResponse(message=f"Cleaned {cleaned_count} expired sessions")
|
|
|
|
|
|
@router.post("/sessions/logout", response_model=MessageResponse)
|
|
async def logout_current_session(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Logout current user session."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = (
|
|
update(UserSession)
|
|
.where(
|
|
UserSession.user_id == user_id,
|
|
UserSession.is_active == True
|
|
)
|
|
.values(is_active=False)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
return MessageResponse(message="Logged out successfully")
|
|
|
|
|
|
# ===== Deck Versions =====
|
|
|
|
@router.post("/decks/{deck_id}/versions", response_model=DeckVersionResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_deck_version(
|
|
deck_id: int,
|
|
request: DeckVersionCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Create a new version of a deck."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify user owns the deck
|
|
deck_check = select(User).where(User.id == user_id)
|
|
deck_result = await db.execute(deck_check)
|
|
user = deck_result.scalar_one_or_none()
|
|
|
|
if not user:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
|
|
|
# Get current version number
|
|
version_stmt = (
|
|
select(func.max(DeckVersion.version_number))
|
|
.where(DeckVersion.deck_id == deck_id)
|
|
)
|
|
version_result = await db.execute(version_stmt)
|
|
max_version = version_result.scalar() or 0
|
|
|
|
new_version = DeckVersion(
|
|
deck_id=deck_id,
|
|
version_number=max_version + 1,
|
|
content=request.content,
|
|
status=request.status.value,
|
|
comment=request.comment,
|
|
created_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(new_version)
|
|
await db.flush()
|
|
|
|
return DeckVersionResponse.model_validate(new_version)
|
|
|
|
|
|
@router.get("/decks/{deck_id}/versions", response_model=DeckVersionListResponse)
|
|
async def get_deck_versions(
|
|
deck_id: int,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get all versions of a deck."""
|
|
offset = (page - 1) * page_size
|
|
|
|
count_stmt = select(func.count()).select_from(DeckVersion).where(DeckVersion.deck_id == deck_id)
|
|
total_result = await db.execute(count_stmt)
|
|
total = total_result.scalar()
|
|
|
|
stmt = (
|
|
select(DeckVersion)
|
|
.where(DeckVersion.deck_id == deck_id)
|
|
.order_by(DeckVersion.version_number.desc())
|
|
.offset(offset)
|
|
.limit(page_size)
|
|
)
|
|
result = await db.execute(stmt)
|
|
versions = result.scalars().all()
|
|
|
|
return DeckVersionListResponse(
|
|
versions=[DeckVersionResponse.model_validate(v) for v in versions],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size
|
|
)
|
|
|
|
|
|
@router.patch("/decks/{deck_id}/versions/{version_id}", response_model=DeckVersionResponse)
|
|
async def update_deck_version(
|
|
deck_id: int,
|
|
version_id: int,
|
|
request: DeckVersionUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update a deck version."""
|
|
stmt = select(DeckVersion).where(DeckVersion.id == version_id)
|
|
result = await db.execute(stmt)
|
|
version = result.scalar_one_or_none()
|
|
|
|
if not version:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Version not found")
|
|
|
|
update_data = request.model_dump(exclude_unset=True)
|
|
if "status" in update_data:
|
|
update_data["status"] = update_data["status"].value
|
|
|
|
stmt = (
|
|
update(DeckVersion)
|
|
.where(DeckVersion.id == version_id)
|
|
.values(**update_data)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
stmt = select(DeckVersion).where(DeckVersion.id == version_id)
|
|
result = await db.execute(stmt)
|
|
updated_version = result.scalar_one_or_none()
|
|
|
|
return DeckVersionResponse.model_validate(updated_version)
|
|
|
|
|
|
@router.delete("/decks/{deck_id}/versions/{version_id}", response_model=MessageResponse)
|
|
async def delete_deck_version(
|
|
deck_id: int,
|
|
version_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete a deck version."""
|
|
stmt = select(DeckVersion).where(DeckVersion.id == version_id)
|
|
result = await db.execute(stmt)
|
|
version = result.scalar_one_or_none()
|
|
|
|
if not version:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Version not found")
|
|
|
|
stmt = delete(DeckVersion).where(DeckVersion.id == version_id)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
return MessageResponse(message="Version deleted successfully")
|
|
|
|
|
|
# ===== Game Replays =====
|
|
|
|
@router.post("/replays", response_model=GameReplayResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_game_replay(
|
|
request: GameReplayCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Create a new game replay."""
|
|
# Check if replay already exists
|
|
stmt = select(GameReplay).where(GameReplay.game_uuid == request.game_uuid)
|
|
result = await db.execute(stmt)
|
|
existing = result.scalar_one_or_none()
|
|
|
|
if existing:
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Replay already exists")
|
|
|
|
replay = GameReplay(
|
|
game_uuid=request.game_uuid,
|
|
room_id=request.room_id,
|
|
game_type=request.game_type,
|
|
format=request.format,
|
|
duration_seconds=request.duration_seconds,
|
|
start_time=request.start_time,
|
|
end_time=request.end_time,
|
|
status=request.status.value,
|
|
replay_data=request.replay_data,
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(replay)
|
|
await db.flush()
|
|
|
|
return GameReplayResponse.model_validate(replay)
|
|
|
|
|
|
@router.get("/replays", response_model=GameReplayListResponse)
|
|
async def get_game_replays(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
user_id: Optional[int] = None,
|
|
status_filter: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get game replays with optional filtering."""
|
|
offset = (page - 1) * page_size
|
|
|
|
# Build WHERE clause
|
|
conditions = []
|
|
if user_id:
|
|
conditions.append(ReplayPlayer.user_id == user_id)
|
|
if status_filter:
|
|
conditions.append(GameReplay.status == status_filter)
|
|
|
|
# Get total count
|
|
count_stmt = select(func.count()).select_from(GameReplay)
|
|
if conditions:
|
|
count_stmt = count_stmt.join(ReplayPlayer).where(*conditions)
|
|
total_result = await db.execute(count_stmt)
|
|
total = total_result.scalar()
|
|
|
|
# Get replays
|
|
stmt = select(GameReplay)
|
|
if conditions:
|
|
stmt = stmt.join(ReplayPlayer).where(*conditions)
|
|
stmt = stmt.order_by(GameReplay.start_time.desc()).offset(offset).limit(page_size)
|
|
|
|
result = await db.execute(stmt)
|
|
replays = result.scalars().all()
|
|
|
|
return GameReplayListResponse(
|
|
replays=[GameReplayResponse.model_validate(r) for r in replays],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size
|
|
)
|
|
|
|
|
|
@router.get("/replays/{replay_id}", response_model=GameReplayResponse)
|
|
async def get_game_replay(
|
|
replay_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get a specific game replay."""
|
|
stmt = select(GameReplay).where(GameReplay.id == replay_id)
|
|
result = await db.execute(stmt)
|
|
replay = result.scalar_one_or_none()
|
|
|
|
if not replay:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Replay not found")
|
|
|
|
return GameReplayResponse.model_validate(replay)
|
|
|
|
|
|
@router.patch("/replays/{replay_id}", response_model=GameReplayResponse)
|
|
async def update_game_replay(
|
|
replay_id: int,
|
|
request: GameReplayUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update a game replay."""
|
|
stmt = select(GameReplay).where(GameReplay.id == replay_id)
|
|
result = await db.execute(stmt)
|
|
replay = result.scalar_one_or_none()
|
|
|
|
if not replay:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Replay not found")
|
|
|
|
update_data = request.model_dump(exclude_unset=True)
|
|
if "status" in update_data:
|
|
update_data["status"] = update_data["status"].value
|
|
update_data["updated_at"] = datetime.now(timezone.utc)
|
|
|
|
stmt = (
|
|
update(GameReplay)
|
|
.where(GameReplay.id == replay_id)
|
|
.values(**update_data)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
stmt = select(GameReplay).where(GameReplay.id == replay_id)
|
|
result = await db.execute(stmt)
|
|
updated_replay = result.scalar_one_or_none()
|
|
|
|
return GameReplayResponse.model_validate(updated_replay)
|
|
|
|
|
|
@router.delete("/replays/{replay_id}", response_model=MessageResponse)
|
|
async def delete_game_replay(
|
|
replay_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete a game replay."""
|
|
stmt = select(GameReplay).where(GameReplay.id == replay_id)
|
|
result = await db.execute(stmt)
|
|
replay = result.scalar_one_or_none()
|
|
|
|
if not replay:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Replay not found")
|
|
|
|
stmt = delete(GameReplay).where(GameReplay.id == replay_id)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
return MessageResponse(message="Replay deleted successfully")
|
|
|
|
|
|
# ===== Replay Players =====
|
|
|
|
@router.post("/replays/{replay_id}/players", status_code=status.HTTP_201_CREATED)
|
|
async def add_replay_player(
|
|
replay_id: int,
|
|
user_id: int,
|
|
deck_id: Optional[int] = None,
|
|
position: Optional[int] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Add a player to a game replay."""
|
|
# Verify replay exists
|
|
stmt = select(GameReplay).where(GameReplay.id == replay_id)
|
|
result = await db.execute(stmt)
|
|
replay = result.scalar_one_or_none()
|
|
|
|
if not replay:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Replay not found")
|
|
|
|
# Check if player already exists
|
|
player_check = (
|
|
select(ReplayPlayer)
|
|
.where(ReplayPlayer.replay_id == replay_id, ReplayPlayer.user_id == user_id)
|
|
)
|
|
player_result = await db.execute(player_check)
|
|
if player_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Player already in replay")
|
|
|
|
player = ReplayPlayer(
|
|
replay_id=replay_id,
|
|
user_id=user_id,
|
|
deck_id=deck_id,
|
|
position=position,
|
|
created_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(player)
|
|
await db.flush()
|
|
|
|
return {"message": "Player added to replay", "player_id": player.id}
|
|
|
|
|
|
@router.get("/replays/{replay_id}/players", response_model=List[dict])
|
|
async def get_replay_players(
|
|
replay_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get all players in a game replay."""
|
|
stmt = select(ReplayPlayer).where(ReplayPlayer.replay_id == replay_id)
|
|
result = await db.execute(stmt)
|
|
players = result.scalars().all()
|
|
|
|
return [
|
|
{
|
|
"id": p.id,
|
|
"user_id": p.user_id,
|
|
"deck_id": p.deck_id,
|
|
"position": p.position,
|
|
"won": p.won,
|
|
"lost": p.lost,
|
|
"concession": p.concession,
|
|
"turn_one": p.turn_one,
|
|
}
|
|
for p in players
|
|
]
|
|
|
|
|
|
# ===== Game Outcomes =====
|
|
|
|
@router.post("/outcomes", response_model=GameOutcomeResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_game_outcome(
|
|
request: GameOutcomeCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Create a game outcome record."""
|
|
outcome = GameOutcome(
|
|
user_id=int(current_user["user_id"]),
|
|
game_uuid=request.game_uuid,
|
|
outcome=request.outcome.value,
|
|
opponent_id=request.opponent_id,
|
|
format=request.format,
|
|
rating_before=request.rating_before,
|
|
rating_after=request.rating_after,
|
|
rating_change=request.rating_change,
|
|
created_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(outcome)
|
|
await db.flush()
|
|
|
|
return GameOutcomeResponse.model_validate(outcome)
|
|
|
|
|
|
@router.get("/outcomes", response_model=GameOutcomeListResponse)
|
|
async def get_game_outcomes(
|
|
user_id: Optional[int] = None,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get game outcomes with optional filtering."""
|
|
offset = (page - 1) * page_size
|
|
|
|
stmt = select(GameOutcome)
|
|
if user_id:
|
|
stmt = stmt.where(GameOutcome.user_id == user_id)
|
|
stmt = stmt.order_by(GameOutcome.created_at.desc()).offset(offset).limit(page_size)
|
|
|
|
result = await db.execute(stmt)
|
|
outcomes = result.scalars().all()
|
|
|
|
count_stmt = select(func.count()).select_from(GameOutcome)
|
|
if user_id:
|
|
count_stmt = count_stmt.where(GameOutcome.user_id == user_id)
|
|
total_result = await db.execute(count_stmt)
|
|
total = total_result.scalar()
|
|
|
|
return GameOutcomeListResponse(
|
|
outcomes=[GameOutcomeResponse.model_validate(o) for o in outcomes],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size
|
|
)
|
|
|
|
|
|
# ===== User Statistics =====
|
|
|
|
@router.get("/statistics/{user_id}", response_model=UserStatisticsResponse)
|
|
async def get_user_statistics(
|
|
user_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get user game statistics."""
|
|
stmt = select(UserStatistics).where(UserStatistics.user_id == user_id)
|
|
result = await db.execute(stmt)
|
|
stats = result.scalar_one_or_none()
|
|
|
|
if not stats:
|
|
# Create default statistics
|
|
stats = UserStatistics(
|
|
user_id=user_id,
|
|
total_games=0,
|
|
total_wins=0,
|
|
total_losses=0,
|
|
total_concessions=0,
|
|
win_rate=0.0,
|
|
current_streak=0,
|
|
best_streak=0,
|
|
average_rating=0.0,
|
|
updated_at=datetime.now(timezone.utc)
|
|
)
|
|
db.add(stats)
|
|
await db.flush()
|
|
|
|
return UserStatisticsResponse.model_validate(stats)
|
|
|
|
|
|
@router.post("/statistics/update", response_model=StatisticsUpdateResponse)
|
|
async def update_user_statistics(
|
|
user_id: int,
|
|
outcome: str, # 'WIN', 'LOSS', 'CONCESSION'
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update user statistics after a game."""
|
|
stmt = select(UserStatistics).where(UserStatistics.user_id == user_id)
|
|
result = await db.execute(stmt)
|
|
stats = result.scalar_one_or_none()
|
|
|
|
if not stats:
|
|
stats = UserStatistics(
|
|
user_id=user_id,
|
|
total_games=0,
|
|
total_wins=0,
|
|
total_losses=0,
|
|
total_concessions=0,
|
|
win_rate=0.0,
|
|
current_streak=0,
|
|
best_streak=0,
|
|
average_rating=0.0,
|
|
updated_at=datetime.now(timezone.utc)
|
|
)
|
|
db.add(stats)
|
|
|
|
# Update statistics
|
|
stats.total_games += 1
|
|
stats.last_game_date = datetime.now(timezone.utc)
|
|
|
|
if outcome == "WIN":
|
|
stats.total_wins += 1
|
|
stats.current_streak = max(0, stats.current_streak) + 1
|
|
if stats.current_streak > stats.best_streak:
|
|
stats.best_streak = stats.current_streak
|
|
elif outcome in ("LOSS", "CONCESSION"):
|
|
stats.total_losses += 1
|
|
stats.total_concessions += 1 if outcome == "CONCESSION" else 0
|
|
stats.current_streak = 0
|
|
|
|
# Calculate win rate
|
|
if stats.total_games > 0:
|
|
stats.win_rate = (stats.total_wins / stats.total_games) * 100
|
|
|
|
stats.updated_at = datetime.now(timezone.utc)
|
|
await db.flush()
|
|
|
|
return StatisticsUpdateResponse(
|
|
user_id=stats.user_id,
|
|
total_games=stats.total_games,
|
|
total_wins=stats.total_wins,
|
|
total_losses=stats.total_losses,
|
|
win_rate=stats.win_rate,
|
|
current_streak=stats.current_streak,
|
|
updated_at=stats.updated_at
|
|
)
|
|
|
|
|
|
# ===== Card Collection =====
|
|
|
|
@router.post("/collection", response_model=CardCollectionResponse, status_code=status.HTTP_201_CREATED)
|
|
async def add_card_to_collection(
|
|
request: CardCollectionCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Add a card to user collection."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Check if card already exists in collection
|
|
check = (
|
|
select(UserCardCollection)
|
|
.where(
|
|
UserCardCollection.user_id == user_id,
|
|
UserCardCollection.card_id == request.card_id,
|
|
UserCardCollection.is_foil == request.is_foil,
|
|
UserCardCollection.is_alt_art == request.is_alt_art,
|
|
)
|
|
)
|
|
check_result = await db.execute(check)
|
|
existing = check_result.scalar_one_or_none()
|
|
|
|
if existing:
|
|
# Update quantity
|
|
stmt = (
|
|
update(UserCardCollection)
|
|
.where(UserCardCollection.id == existing.id)
|
|
.values(quantity=existing.quantity + request.quantity)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
stmt = select(UserCardCollection).where(UserCardCollection.id == existing.id)
|
|
result = await db.execute(stmt)
|
|
updated = result.scalar_one_or_none()
|
|
return CardCollectionResponse.model_validate(updated)
|
|
|
|
card = UserCardCollection(
|
|
user_id=user_id,
|
|
card_id=request.card_id,
|
|
quantity=request.quantity,
|
|
condition=request.condition,
|
|
language=request.language,
|
|
is_foil=request.is_foil,
|
|
is_alt_art=request.is_alt_art,
|
|
acquired_date=request.acquired_date or datetime.now(timezone.utc),
|
|
acquisition_method=request.acquisition_method,
|
|
notes=request.notes,
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(card)
|
|
await db.flush()
|
|
|
|
return CardCollectionResponse.model_validate(card)
|
|
|
|
|
|
@router.get("/collection", response_model=CardCollectionListResponse)
|
|
async def get_user_collection(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
is_foil: Optional[bool] = None,
|
|
is_alt_art: Optional[bool] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get user's card collection."""
|
|
user_id = int(current_user["user_id"])
|
|
offset = (page - 1) * page_size
|
|
|
|
conditions = [UserCardCollection.user_id == user_id]
|
|
if is_foil is not None:
|
|
conditions.append(UserCardCollection.is_foil == is_foil)
|
|
if is_alt_art is not None:
|
|
conditions.append(UserCardCollection.is_alt_art == is_alt_art)
|
|
|
|
count_stmt = select(func.count()).select_from(UserCardCollection)
|
|
for cond in conditions:
|
|
count_stmt = count_stmt.where(cond)
|
|
total_result = await db.execute(count_stmt)
|
|
total = total_result.scalar()
|
|
|
|
stmt = select(UserCardCollection)
|
|
for cond in conditions:
|
|
stmt = stmt.where(cond)
|
|
stmt = stmt.order_by(UserCardCollection.acquired_date.desc()).offset(offset).limit(page_size)
|
|
|
|
result = await db.execute(stmt)
|
|
cards = result.scalars().all()
|
|
|
|
return CardCollectionListResponse(
|
|
cards=[CardCollectionResponse.model_validate(c) for c in cards],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size
|
|
)
|
|
|
|
|
|
@router.patch("/collection/{card_id}", response_model=CardCollectionResponse)
|
|
async def update_card_in_collection(
|
|
card_id: int,
|
|
request: CardCollectionUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update a card in user collection."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = select(UserCardCollection).where(
|
|
UserCardCollection.id == card_id,
|
|
UserCardCollection.user_id == user_id
|
|
)
|
|
result = await db.execute(stmt)
|
|
card = result.scalar_one_or_none()
|
|
|
|
if not card:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Card not found in collection")
|
|
|
|
update_data = request.model_dump(exclude_unset=True)
|
|
update_data["updated_at"] = datetime.now(timezone.utc)
|
|
|
|
stmt = (
|
|
update(UserCardCollection)
|
|
.where(UserCardCollection.id == card_id)
|
|
.values(**update_data)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
stmt = select(UserCardCollection).where(UserCardCollection.id == card_id)
|
|
result = await db.execute(stmt)
|
|
updated = result.scalar_one_or_none()
|
|
|
|
return CardCollectionResponse.model_validate(updated)
|
|
|
|
|
|
@router.delete("/collection/{card_id}", response_model=MessageResponse)
|
|
async def remove_card_from_collection(
|
|
card_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Remove a card from user collection."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = select(UserCardCollection).where(
|
|
UserCardCollection.id == card_id,
|
|
UserCardCollection.user_id == user_id
|
|
)
|
|
result = await db.execute(stmt)
|
|
card = result.scalar_one_or_none()
|
|
|
|
if not card:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Card not found in collection")
|
|
|
|
stmt = delete(UserCardCollection).where(UserCardCollection.id == card_id)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
return MessageResponse(message="Card removed from collection")
|
|
|
|
|
|
# ===== Wishlist =====
|
|
|
|
@router.post("/wishlist", response_model=WishlistResponse, status_code=status.HTTP_201_CREATED)
|
|
async def add_to_wishlist(
|
|
request: WishlistCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Add a card to wishlist."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Check if already in wishlist
|
|
check = (
|
|
select(CardWishlist)
|
|
.where(CardWishlist.user_id == user_id, CardWishlist.card_id == request.card_id)
|
|
)
|
|
check_result = await db.execute(check)
|
|
if check_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Card already in wishlist")
|
|
|
|
item = CardWishlist(
|
|
user_id=user_id,
|
|
card_id=request.card_id,
|
|
max_price=request.max_price,
|
|
notes=request.notes,
|
|
created_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(item)
|
|
await db.flush()
|
|
|
|
return WishlistResponse.model_validate(item)
|
|
|
|
|
|
@router.get("/wishlist", response_model=WishlistListResponse)
|
|
async def get_wishlist(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get user's wishlist."""
|
|
user_id = int(current_user["user_id"])
|
|
offset = (page - 1) * page_size
|
|
|
|
count_stmt = select(func.count()).select_from(CardWishlist).where(CardWishlist.user_id == user_id)
|
|
total_result = await db.execute(count_stmt)
|
|
total = total_result.scalar()
|
|
|
|
stmt = (
|
|
select(CardWishlist)
|
|
.where(CardWishlist.user_id == user_id)
|
|
.order_by(CardWishlist.created_at.desc())
|
|
.offset(offset)
|
|
.limit(page_size)
|
|
)
|
|
result = await db.execute(stmt)
|
|
items = result.scalars().all()
|
|
|
|
return WishlistListResponse(
|
|
items=[WishlistResponse.model_validate(i) for i in items],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size
|
|
)
|
|
|
|
|
|
@router.patch("/wishlist/{item_id}", response_model=WishlistResponse)
|
|
async def update_wishlist_item(
|
|
item_id: int,
|
|
request: WishlistUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update a wishlist item."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = select(CardWishlist).where(
|
|
CardWishlist.id == item_id,
|
|
CardWishlist.user_id == user_id
|
|
)
|
|
result = await db.execute(stmt)
|
|
item = result.scalar_one_or_none()
|
|
|
|
if not item:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Wishlist item not found")
|
|
|
|
update_data = request.model_dump(exclude_unset=True)
|
|
|
|
stmt = (
|
|
update(CardWishlist)
|
|
.where(CardWishlist.id == item_id)
|
|
.values(**update_data)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
stmt = select(CardWishlist).where(CardWishlist.id == item_id)
|
|
result = await db.execute(stmt)
|
|
updated = result.scalar_one_or_none()
|
|
|
|
return WishlistResponse.model_validate(updated)
|
|
|
|
|
|
@router.delete("/wishlist/{item_id}", response_model=MessageResponse)
|
|
async def remove_from_wishlist(
|
|
item_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Remove a card from wishlist."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = select(CardWishlist).where(
|
|
CardWishlist.id == item_id,
|
|
CardWishlist.user_id == user_id
|
|
)
|
|
result = await db.execute(stmt)
|
|
item = result.scalar_one_or_none()
|
|
|
|
if not item:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Wishlist item not found")
|
|
|
|
stmt = delete(CardWishlist).where(CardWishlist.id == item_id)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
return MessageResponse(message="Removed from wishlist")
|
|
|
|
|
|
# ===== Groups =====
|
|
|
|
@router.post("/groups", response_model=GroupResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_group(
|
|
request: GroupCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Create a new user group."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
group = UserGroup(
|
|
name=request.name,
|
|
description=request.description,
|
|
owner_id=user_id,
|
|
is_public=request.is_public,
|
|
max_members=request.max_members,
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(group)
|
|
await db.flush()
|
|
|
|
# Add creator as owner
|
|
member = GroupMember(
|
|
group_id=group.id,
|
|
user_id=user_id,
|
|
role="OWNER",
|
|
joined_at=datetime.now(timezone.utc)
|
|
)
|
|
db.add(member)
|
|
await db.flush()
|
|
|
|
return GroupResponse.model_validate(group)
|
|
|
|
|
|
@router.get("/groups", response_model=GroupListResponse)
|
|
async def get_user_groups(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
is_public: Optional[bool] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get user's groups."""
|
|
user_id = int(current_user["user_id"])
|
|
offset = (page - 1) * page_size
|
|
|
|
# Get groups where user is member or owner
|
|
member_groups = select(GroupMember.group_id).where(GroupMember.user_id == user_id)
|
|
|
|
count_stmt = select(func.count()).select_from(UserGroup).where(
|
|
UserGroup.id.in_(member_groups)
|
|
)
|
|
if is_public is not None:
|
|
count_stmt = count_stmt.where(UserGroup.is_public == is_public)
|
|
total_result = await db.execute(count_stmt)
|
|
total = total_result.scalar()
|
|
|
|
stmt = (
|
|
select(UserGroup)
|
|
.where(UserGroup.id.in_(member_groups))
|
|
.order_by(UserGroup.created_at.desc())
|
|
.offset(offset)
|
|
.limit(page_size)
|
|
)
|
|
result = await db.execute(stmt)
|
|
groups = result.scalars().all()
|
|
|
|
# Get member counts
|
|
group_ids = [g.id for g in groups]
|
|
member_counts = {}
|
|
if group_ids:
|
|
count_subquery = (
|
|
select(GroupMember.group_id, func.count().label('count'))
|
|
.where(GroupMember.group_id.in_(group_ids))
|
|
.group_by(GroupMember.group_id)
|
|
.subquery()
|
|
)
|
|
count_stmt = select(count_subquery.c.group_id, count_subquery.c.count).where(
|
|
count_subquery.c.group_id.in_(group_ids)
|
|
)
|
|
count_result = await db.execute(count_stmt)
|
|
member_counts = {row[0]: row[1] for row in count_result.fetchall()}
|
|
|
|
return GroupListResponse(
|
|
groups=[
|
|
GroupResponse(
|
|
**GroupResponse.model_validate(g).model_dump(),
|
|
member_count=member_counts.get(g.id, 0)
|
|
)
|
|
for g in groups
|
|
],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size
|
|
)
|
|
|
|
|
|
@router.get("/groups/{group_id}", response_model=GroupResponse)
|
|
async def get_group(
|
|
group_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get a specific group."""
|
|
stmt = select(UserGroup).where(UserGroup.id == group_id)
|
|
result = await db.execute(stmt)
|
|
group = result.scalar_one_or_none()
|
|
|
|
if not group:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Group not found")
|
|
|
|
return GroupResponse.model_validate(group)
|
|
|
|
|
|
@router.patch("/groups/{group_id}", response_model=GroupResponse)
|
|
async def update_group(
|
|
group_id: int,
|
|
request: GroupUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update a group."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify user is owner
|
|
check = (
|
|
select(GroupMember)
|
|
.where(
|
|
GroupMember.group_id == group_id,
|
|
GroupMember.user_id == user_id,
|
|
GroupMember.role == "OWNER"
|
|
)
|
|
)
|
|
check_result = await db.execute(check)
|
|
if not check_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only group owner can update group")
|
|
|
|
stmt = select(UserGroup).where(UserGroup.id == group_id)
|
|
result = await db.execute(stmt)
|
|
group = result.scalar_one_or_none()
|
|
|
|
if not group:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Group not found")
|
|
|
|
update_data = request.model_dump(exclude_unset=True)
|
|
update_data["updated_at"] = datetime.now(timezone.utc)
|
|
|
|
stmt = (
|
|
update(UserGroup)
|
|
.where(UserGroup.id == group_id)
|
|
.values(**update_data)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
stmt = select(UserGroup).where(UserGroup.id == group_id)
|
|
result = await db.execute(stmt)
|
|
updated = result.scalar_one_or_none()
|
|
|
|
return GroupResponse.model_validate(updated)
|
|
|
|
|
|
@router.delete("/groups/{group_id}", response_model=MessageResponse)
|
|
async def delete_group(
|
|
group_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete a group."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify user is owner
|
|
check = (
|
|
select(GroupMember)
|
|
.where(
|
|
GroupMember.group_id == group_id,
|
|
GroupMember.user_id == user_id,
|
|
GroupMember.role == "OWNER"
|
|
)
|
|
)
|
|
check_result = await db.execute(check)
|
|
if not check_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only group owner can delete group")
|
|
|
|
stmt = delete(UserGroup).where(UserGroup.id == group_id)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
return MessageResponse(message="Group deleted successfully")
|
|
|
|
|
|
# ===== Group Members =====
|
|
|
|
@router.post("/groups/{group_id}/members", status_code=status.HTTP_201_CREATED)
|
|
async def add_group_member(
|
|
group_id: int,
|
|
request: GroupMemberCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Add a member to a group."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify user is admin or owner
|
|
check = (
|
|
select(GroupMember)
|
|
.where(
|
|
GroupMember.group_id == group_id,
|
|
GroupMember.user_id == user_id,
|
|
GroupMember.role.in_(["OWNER", "ADMIN"])
|
|
)
|
|
)
|
|
check_result = await db.execute(check)
|
|
if not check_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required")
|
|
|
|
# Check if already a member
|
|
member_check = (
|
|
select(GroupMember)
|
|
.where(GroupMember.group_id == group_id, GroupMember.user_id == request.user_id)
|
|
)
|
|
member_result = await db.execute(member_check)
|
|
if member_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Already a member")
|
|
|
|
member = GroupMember(
|
|
group_id=group_id,
|
|
user_id=request.user_id,
|
|
role=request.role.value,
|
|
joined_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(member)
|
|
await db.flush()
|
|
|
|
return {"message": "Member added", "member_id": member.id}
|
|
|
|
|
|
@router.patch("/groups/{group_id}/members/{member_id}", response_model=GroupMemberUpdate)
|
|
async def update_group_member(
|
|
group_id: int,
|
|
member_id: int,
|
|
request: GroupMemberUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update a group member's role."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify user is owner
|
|
check = (
|
|
select(GroupMember)
|
|
.where(
|
|
GroupMember.group_id == group_id,
|
|
GroupMember.user_id == user_id,
|
|
GroupMember.role == "OWNER"
|
|
)
|
|
)
|
|
check_result = await db.execute(check)
|
|
if not check_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only group owner can update roles")
|
|
|
|
stmt = select(GroupMember).where(GroupMember.id == member_id)
|
|
result = await db.execute(stmt)
|
|
member = result.scalar_one_or_none()
|
|
|
|
if not member:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Member not found")
|
|
|
|
stmt = (
|
|
update(GroupMember)
|
|
.where(GroupMember.id == member_id)
|
|
.values(role=request.role.value)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
return {"message": "Member role updated"}
|
|
|
|
|
|
@router.delete("/groups/{group_id}/members/{member_id}", response_model=MessageResponse)
|
|
async def remove_group_member(
|
|
group_id: int,
|
|
member_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Remove a member from a group."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify user is admin or owner
|
|
check = (
|
|
select(GroupMember)
|
|
.where(
|
|
GroupMember.group_id == group_id,
|
|
GroupMember.user_id == user_id,
|
|
GroupMember.role.in_(["OWNER", "ADMIN"])
|
|
)
|
|
)
|
|
check_result = await db.execute(check)
|
|
if not check_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required")
|
|
|
|
stmt = select(GroupMember).where(GroupMember.id == member_id)
|
|
result = await db.execute(stmt)
|
|
member = result.scalar_one_or_none()
|
|
|
|
if not member:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Member not found")
|
|
|
|
stmt = delete(GroupMember).where(GroupMember.id == member_id)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
return MessageResponse(message="Member removed from group")
|
|
|
|
|
|
# ===== Group Chat Messages =====
|
|
|
|
@router.post("/groups/{group_id}/messages", response_model=GroupChatMessageResponse, status_code=status.HTTP_201_CREATED)
|
|
async def send_group_message(
|
|
group_id: int,
|
|
request: GroupChatMessageCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Send a message in a group chat."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify user is a member
|
|
member_check = (
|
|
select(GroupMember)
|
|
.where(GroupMember.group_id == group_id, GroupMember.user_id == user_id)
|
|
)
|
|
member_result = await db.execute(member_check)
|
|
if not member_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a group member")
|
|
|
|
# Verify group exists
|
|
group_check = select(UserGroup).where(UserGroup.id == group_id)
|
|
group_result = await db.execute(group_check)
|
|
if not group_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Group not found")
|
|
|
|
message = GroupChatMessage(
|
|
group_id=group_id,
|
|
sender_id=user_id,
|
|
message=request.message,
|
|
created_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(message)
|
|
await db.flush()
|
|
|
|
# Get sender username
|
|
user_check = select(User).where(User.id == user_id)
|
|
user_result = await db.execute(user_check)
|
|
sender = user_result.scalar_one_or_none()
|
|
|
|
return GroupChatMessageResponse(
|
|
id=message.id,
|
|
group_id=message.group_id,
|
|
sender_id=message.sender_id,
|
|
sender_username=sender.username if sender else None,
|
|
message=message.message,
|
|
created_at=message.created_at
|
|
)
|
|
|
|
|
|
@router.get("/groups/{group_id}/messages", response_model=GroupChatMessageListResponse)
|
|
async def get_group_messages(
|
|
group_id: int,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get messages in a group chat."""
|
|
offset = (page - 1) * page_size
|
|
|
|
# Verify user is a member
|
|
member_check = (
|
|
select(GroupMember)
|
|
.where(GroupMember.group_id == group_id, GroupMember.user_id == int(current_user["user_id"]))
|
|
)
|
|
member_result = await db.execute(member_check)
|
|
if not member_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a group member")
|
|
|
|
count_stmt = select(func.count()).select_from(GroupChatMessage).where(GroupChatMessage.group_id == group_id)
|
|
total_result = await db.execute(count_stmt)
|
|
total = total_result.scalar()
|
|
|
|
stmt = (
|
|
select(GroupChatMessage)
|
|
.where(GroupChatMessage.group_id == group_id)
|
|
.order_by(GroupChatMessage.created_at.desc())
|
|
.offset(offset)
|
|
.limit(page_size)
|
|
)
|
|
result = await db.execute(stmt)
|
|
messages = result.scalars().all()
|
|
|
|
# Get sender usernames
|
|
sender_ids = [m.sender_id for m in messages]
|
|
sender_names = {}
|
|
if sender_ids:
|
|
user_stmt = select(User.id, User.username).where(User.id.in_(sender_ids))
|
|
user_result = await db.execute(user_stmt)
|
|
sender_names = {row[0]: row[1] for row in user_result.fetchall()}
|
|
|
|
return GroupChatMessageListResponse(
|
|
messages=[
|
|
GroupChatMessageResponse(
|
|
id=m.id,
|
|
group_id=m.group_id,
|
|
sender_id=m.sender_id,
|
|
sender_username=sender_names.get(m.sender_id),
|
|
message=m.message,
|
|
created_at=m.created_at
|
|
)
|
|
for m in messages
|
|
],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size
|
|
)
|
|
|
|
|
|
# ===== Networks =====
|
|
|
|
@router.post("/networks", response_model=NetworkResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_network(
|
|
request: NetworkCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Create a new user network."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
network = UserNetwork(
|
|
name=request.name,
|
|
description=request.description,
|
|
creator_id=user_id,
|
|
is_public=request.is_public,
|
|
created_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(network)
|
|
await db.flush()
|
|
|
|
# Add creator as owner
|
|
member = NetworkMember(
|
|
network_id=network.id,
|
|
user_id=user_id,
|
|
role="OWNER",
|
|
joined_at=datetime.now(timezone.utc)
|
|
)
|
|
db.add(member)
|
|
await db.flush()
|
|
|
|
return NetworkResponse.model_validate(network)
|
|
|
|
|
|
@router.get("/networks", response_model=NetworkListResponse)
|
|
async def get_user_networks(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get user's networks."""
|
|
user_id = int(current_user["user_id"])
|
|
offset = (page - 1) * page_size
|
|
|
|
member_networks = select(NetworkMember.network_id).where(NetworkMember.user_id == user_id)
|
|
|
|
count_stmt = select(func.count()).select_from(UserNetwork).where(
|
|
UserNetwork.id.in_(member_networks)
|
|
)
|
|
total_result = await db.execute(count_stmt)
|
|
total = total_result.scalar()
|
|
|
|
stmt = (
|
|
select(UserNetwork)
|
|
.where(UserNetwork.id.in_(member_networks))
|
|
.order_by(UserNetwork.created_at.desc())
|
|
.offset(offset)
|
|
.limit(page_size)
|
|
)
|
|
result = await db.execute(stmt)
|
|
networks = result.scalars().all()
|
|
|
|
# Get member counts
|
|
network_ids = [n.id for n in networks]
|
|
member_counts = {}
|
|
if network_ids:
|
|
count_subquery = (
|
|
select(NetworkMember.network_id, func.count().label('count'))
|
|
.where(NetworkMember.network_id.in_(network_ids))
|
|
.group_by(NetworkMember.network_id)
|
|
.subquery()
|
|
)
|
|
count_stmt = select(count_subquery.c.network_id, count_subquery.c.count).where(
|
|
count_subquery.c.network_id.in_(network_ids)
|
|
)
|
|
count_result = await db.execute(count_stmt)
|
|
member_counts = {row[0]: row[1] for row in count_result.fetchall()}
|
|
|
|
return NetworkListResponse(
|
|
networks=[
|
|
NetworkResponse(
|
|
**NetworkResponse.model_validate(n).model_dump(),
|
|
member_count=member_counts.get(n.id, 0)
|
|
)
|
|
for n in networks
|
|
],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size
|
|
)
|
|
|
|
|
|
@router.get("/networks/{network_id}", response_model=NetworkResponse)
|
|
async def get_network(
|
|
network_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get a specific network."""
|
|
stmt = select(UserNetwork).where(UserNetwork.id == network_id)
|
|
result = await db.execute(stmt)
|
|
network = result.scalar_one_or_none()
|
|
|
|
if not network:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Network not found")
|
|
|
|
return NetworkResponse.model_validate(network)
|
|
|
|
|
|
@router.patch("/networks/{network_id}", response_model=NetworkResponse)
|
|
async def update_network(
|
|
network_id: int,
|
|
request: NetworkUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update a network."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify user is creator
|
|
check = (
|
|
select(UserNetwork)
|
|
.where(UserNetwork.id == network_id, UserNetwork.creator_id == user_id)
|
|
)
|
|
check_result = await db.execute(check)
|
|
if not check_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only creator can update network")
|
|
|
|
stmt = select(UserNetwork).where(UserNetwork.id == network_id)
|
|
result = await db.execute(stmt)
|
|
network = result.scalar_one_or_none()
|
|
|
|
if not network:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Network not found")
|
|
|
|
update_data = request.model_dump(exclude_unset=True)
|
|
|
|
stmt = (
|
|
update(UserNetwork)
|
|
.where(UserNetwork.id == network_id)
|
|
.values(**update_data)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
stmt = select(UserNetwork).where(UserNetwork.id == network_id)
|
|
result = await db.execute(stmt)
|
|
updated = result.scalar_one_or_none()
|
|
|
|
return NetworkResponse.model_validate(updated)
|
|
|
|
|
|
@router.delete("/networks/{network_id}", response_model=MessageResponse)
|
|
async def delete_network(
|
|
network_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete a network."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify user is creator
|
|
check = (
|
|
select(UserNetwork)
|
|
.where(UserNetwork.id == network_id, UserNetwork.creator_id == user_id)
|
|
)
|
|
check_result = await db.execute(check)
|
|
if not check_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only creator can delete network")
|
|
|
|
stmt = delete(UserNetwork).where(UserNetwork.id == network_id)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
return MessageResponse(message="Network deleted successfully")
|
|
|
|
|
|
# ===== Network Members =====
|
|
|
|
@router.post("/networks/{network_id}/members", status_code=status.HTTP_201_CREATED)
|
|
async def add_network_member(
|
|
network_id: int,
|
|
request: NetworkMemberCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Add a member to a network."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify user is admin or owner
|
|
check = (
|
|
select(NetworkMember)
|
|
.where(
|
|
NetworkMember.network_id == network_id,
|
|
NetworkMember.user_id == user_id,
|
|
NetworkMember.role.in_(["OWNER", "ADMIN"])
|
|
)
|
|
)
|
|
check_result = await db.execute(check)
|
|
if not check_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required")
|
|
|
|
# Check if already a member
|
|
member_check = (
|
|
select(NetworkMember)
|
|
.where(NetworkMember.network_id == network_id, NetworkMember.user_id == request.user_id)
|
|
)
|
|
member_result = await db.execute(member_check)
|
|
if member_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Already a member")
|
|
|
|
member = NetworkMember(
|
|
network_id=network_id,
|
|
user_id=request.user_id,
|
|
role=request.role.value,
|
|
joined_at=datetime.now(timezone.utc)
|
|
)
|
|
|
|
db.add(member)
|
|
await db.flush()
|
|
|
|
return {"message": "Member added", "member_id": member.id}
|
|
|
|
|
|
# ===== User Preferences =====
|
|
|
|
@router.get("/preferences", response_model=UserPreferenceResponse)
|
|
async def get_user_preferences(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get user preferences."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = select(UserPreference).where(UserPreference.user_id == user_id)
|
|
result = await db.execute(stmt)
|
|
prefs = result.scalar_one_or_none()
|
|
|
|
if not prefs:
|
|
# Create default preferences
|
|
prefs = UserPreference(
|
|
user_id=user_id,
|
|
theme="light",
|
|
notifications_enabled=True,
|
|
email_notifications=True,
|
|
auto_save_decks=True,
|
|
default_format="standard",
|
|
language="EN",
|
|
updated_at=datetime.now(timezone.utc)
|
|
)
|
|
db.add(prefs)
|
|
await db.flush()
|
|
|
|
return UserPreferenceResponse.model_validate(prefs)
|
|
|
|
|
|
@router.patch("/preferences", response_model=UserPreferenceResponse)
|
|
async def update_user_preferences(
|
|
request: UserPreferenceUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update user preferences."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = select(UserPreference).where(UserPreference.user_id == user_id)
|
|
result = await db.execute(stmt)
|
|
prefs = result.scalar_one_or_none()
|
|
|
|
if not prefs:
|
|
# Create default preferences first
|
|
prefs = UserPreference(
|
|
user_id=user_id,
|
|
updated_at=datetime.now(timezone.utc)
|
|
)
|
|
db.add(prefs)
|
|
await db.flush()
|
|
|
|
update_data = request.model_dump(exclude_unset=True)
|
|
update_data["updated_at"] = datetime.now(timezone.utc)
|
|
|
|
stmt = (
|
|
update(UserPreference)
|
|
.where(UserPreference.user_id == user_id)
|
|
.values(**update_data)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
stmt = select(UserPreference).where(UserPreference.user_id == user_id)
|
|
result = await db.execute(stmt)
|
|
updated = result.scalar_one_or_none()
|
|
|
|
return UserPreferenceResponse.model_validate(updated)
|
|
|
|
|
|
# ===== Activity Log =====
|
|
|
|
@router.get("/activity", response_model=ActivityLogListResponse)
|
|
async def get_activity_log(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=100),
|
|
activity_type: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get user activity log."""
|
|
user_id = int(current_user["user_id"])
|
|
offset = (page - 1) * page_size
|
|
|
|
conditions = [UserActivityLog.user_id == user_id]
|
|
if activity_type:
|
|
conditions.append(UserActivityLog.activity_type == activity_type)
|
|
|
|
count_stmt = select(func.count()).select_from(UserActivityLog)
|
|
for cond in conditions:
|
|
count_stmt = count_stmt.where(cond)
|
|
total_result = await db.execute(count_stmt)
|
|
total = total_result.scalar()
|
|
|
|
stmt = select(UserActivityLog)
|
|
for cond in conditions:
|
|
stmt = stmt.where(cond)
|
|
stmt = stmt.order_by(UserActivityLog.created_at.desc()).offset(offset).limit(page_size)
|
|
|
|
result = await db.execute(stmt)
|
|
entries = result.scalars().all()
|
|
|
|
return ActivityLogListResponse(
|
|
entries=[ActivityLogEntry.model_validate(e) for e in entries],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=(total + page_size - 1) // page_size
|
|
)
|