This commit is contained in:
xds
2026-03-16 12:12:56 +03:00
commit 9d886076d6
63 changed files with 4482 additions and 0 deletions

View File

View File

@@ -0,0 +1,102 @@
import uuid
from pathlib import Path
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.config import settings
from backend.app.core.database import get_session
from backend.app.models.activity import Activity, DataPoint
from backend.app.schemas.activity import (
ActivityResponse,
ActivityListResponse,
DataPointResponse,
)
from backend.app.services.fit_parser import parse_fit_file
from backend.app.services.metrics import calculate_metrics
router = APIRouter()
@router.post("/upload", response_model=ActivityResponse)
async def upload_activity(
rider_id: uuid.UUID,
file: UploadFile = File(...),
session: AsyncSession = Depends(get_session),
):
if not file.filename or not file.filename.lower().endswith(".fit"):
raise HTTPException(status_code=400, detail="Only .FIT files are accepted")
upload_dir = Path(settings.UPLOAD_DIR)
upload_dir.mkdir(parents=True, exist_ok=True)
file_id = uuid.uuid4()
file_path = upload_dir / f"{file_id}.fit"
content = await file.read()
file_path.write_bytes(content)
activity, data_points = parse_fit_file(content, rider_id, str(file_path))
session.add(activity)
await session.flush()
for dp in data_points:
dp.activity_id = activity.id
session.add_all(data_points)
metrics = calculate_metrics(data_points, activity, rider_id, session)
if metrics:
session.add(metrics)
await session.commit()
await session.refresh(activity)
return activity
@router.get("", response_model=ActivityListResponse)
async def list_activities(
rider_id: uuid.UUID,
limit: int = 20,
offset: int = 0,
session: AsyncSession = Depends(get_session),
):
count_query = select(func.count(Activity.id)).where(Activity.rider_id == rider_id)
total = (await session.execute(count_query)).scalar() or 0
query = (
select(Activity)
.where(Activity.rider_id == rider_id)
.order_by(Activity.date.desc())
.limit(limit)
.offset(offset)
)
result = await session.execute(query)
activities = result.scalars().all()
return ActivityListResponse(items=activities, total=total)
@router.get("/{activity_id}", response_model=ActivityResponse)
async def get_activity(
activity_id: uuid.UUID,
session: AsyncSession = Depends(get_session),
):
activity = await session.get(Activity, activity_id)
if not activity:
raise HTTPException(status_code=404, detail="Activity not found")
return activity
@router.get("/{activity_id}/stream", response_model=list[DataPointResponse])
async def get_activity_stream(
activity_id: uuid.UUID,
session: AsyncSession = Depends(get_session),
):
query = (
select(DataPoint)
.where(DataPoint.activity_id == activity_id)
.order_by(DataPoint.timestamp)
)
result = await session.execute(query)
return result.scalars().all()

117
backend/app/api/auth.py Normal file
View File

@@ -0,0 +1,117 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.config import settings
from backend.app.core.database import get_session
from backend.app.core.security import (
create_access_token,
verify_telegram_login,
verify_telegram_webapp,
)
from backend.app.models.rider import Rider
from backend.app.schemas.auth import (
AuthResponse,
TelegramLoginRequest,
TelegramWebAppRequest,
)
router = APIRouter()
async def _upsert_rider(
session: AsyncSession,
telegram_id: int,
first_name: str,
last_name: str | None,
username: str | None,
photo_url: str | None,
) -> Rider:
result = await session.execute(
select(Rider).where(Rider.telegram_id == telegram_id)
)
rider = result.scalar_one_or_none()
name = first_name
if last_name:
name = f"{first_name} {last_name}"
if not rider:
rider = Rider(
telegram_id=telegram_id,
name=name,
telegram_username=username,
avatar_url=photo_url,
)
session.add(rider)
else:
rider.name = name
rider.telegram_username = username
rider.avatar_url = photo_url
await session.commit()
await session.refresh(rider)
return rider
def _build_auth_response(rider: Rider) -> AuthResponse:
token = create_access_token(
rider_id=str(rider.id),
telegram_id=rider.telegram_id,
secret=settings.JWT_SECRET_KEY,
algorithm=settings.JWT_ALGORITHM,
expires_minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES,
)
return AuthResponse(access_token=token, rider=rider)
@router.post("/telegram-login", response_model=AuthResponse)
async def telegram_login(
data: TelegramLoginRequest,
session: AsyncSession = Depends(get_session),
):
if not settings.TELEGRAM_BOT_TOKEN:
raise HTTPException(status_code=500, detail="Telegram bot token not configured")
login_data = data.model_dump()
if not verify_telegram_login(login_data, settings.TELEGRAM_BOT_TOKEN):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Telegram authorization",
)
rider = await _upsert_rider(
session,
telegram_id=data.id,
first_name=data.first_name,
last_name=data.last_name,
username=data.username,
photo_url=data.photo_url,
)
return _build_auth_response(rider)
@router.post("/telegram-webapp", response_model=AuthResponse)
async def telegram_webapp(
data: TelegramWebAppRequest,
session: AsyncSession = Depends(get_session),
):
if not settings.TELEGRAM_BOT_TOKEN:
raise HTTPException(status_code=500, detail="Telegram bot token not configured")
user = verify_telegram_webapp(data.init_data, settings.TELEGRAM_BOT_TOKEN)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Telegram WebApp data",
)
rider = await _upsert_rider(
session,
telegram_id=user["id"],
first_name=user.get("first_name", ""),
last_name=user.get("last_name"),
username=user.get("username"),
photo_url=user.get("photo_url"),
)
return _build_auth_response(rider)

52
backend/app/api/rider.py Normal file
View File

@@ -0,0 +1,52 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.database import get_session
from backend.app.models.rider import Rider
from backend.app.schemas.rider import RiderCreate, RiderUpdate, RiderResponse
router = APIRouter()
@router.post("/profile", response_model=RiderResponse)
async def create_rider(
data: RiderCreate,
session: AsyncSession = Depends(get_session),
):
rider = Rider(**data.model_dump())
session.add(rider)
await session.commit()
await session.refresh(rider)
return rider
@router.get("/profile/{rider_id}", response_model=RiderResponse)
async def get_rider(
rider_id: uuid.UUID,
session: AsyncSession = Depends(get_session),
):
rider = await session.get(Rider, rider_id)
if not rider:
raise HTTPException(status_code=404, detail="Rider not found")
return rider
@router.put("/profile/{rider_id}", response_model=RiderResponse)
async def update_rider(
rider_id: uuid.UUID,
data: RiderUpdate,
session: AsyncSession = Depends(get_session),
):
rider = await session.get(Rider, rider_id)
if not rider:
raise HTTPException(status_code=404, detail="Rider not found")
update_data = data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(rider, key, value)
await session.commit()
await session.refresh(rider)
return rider

11
backend/app/api/router.py Normal file
View File

@@ -0,0 +1,11 @@
from fastapi import APIRouter
from backend.app.api.auth import router as auth_router
from backend.app.api.activities import router as activities_router
from backend.app.api.rider import router as rider_router
api_router = APIRouter(prefix="/api")
api_router.include_router(auth_router, prefix="/auth", tags=["auth"])
api_router.include_router(activities_router, prefix="/activities", tags=["activities"])
api_router.include_router(rider_router, prefix="/rider", tags=["rider"])