harden telegram webapp production readiness

This commit is contained in:
VPN SaaS Dev
2026-05-12 19:14:21 +09:00
parent e75697f83e
commit 2ba2e88432
27 changed files with 931 additions and 155 deletions

View File

@@ -1,9 +1,14 @@
from datetime import date, timedelta
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import (
get_current_telegram_user,
get_or_create_telegram_user,
require_internal_api_token,
)
from app.core.config import settings
from app.db.session import get_session
from app.models.car import Car
@@ -29,41 +34,14 @@ def username_from_telegram(telegram_id: int, username: str | None = None) -> str
return str(telegram_id) if not username else str(telegram_id)
async def upsert_telegram_user(
session: AsyncSession,
*,
telegram_id: int,
username: str | None = None,
first_name: str | None = None,
last_name: str | None = None,
locale: str | None = None,
currency: str | None = None,
) -> User:
result = await session.execute(select(User).where(User.telegram_id == telegram_id))
user = result.scalar_one_or_none()
payload = {
"telegram_id": telegram_id,
"username": username_from_telegram(telegram_id, username),
"first_name": first_name,
"last_name": last_name,
"locale": locale,
"currency": currency,
}
if user is None:
user = User(**{key: value for key, value in payload.items() if value is not None})
session.add(user)
else:
for field, value in payload.items():
if value is not None:
setattr(user, field, value)
await session.commit()
await session.refresh(user)
return user
@router.post("", response_model=UserRead)
async def upsert_user(payload: UserUpsert, session: AsyncSession = Depends(get_session)) -> User:
return await upsert_telegram_user(session, **payload.model_dump())
async def upsert_user(
payload: UserUpsert,
session: AsyncSession = Depends(get_session),
x_internal_api_token: str | None = Header(default=None, alias="X-Internal-API-Token"),
) -> User:
require_internal_api_token(x_internal_api_token)
return await get_or_create_telegram_user(session, **payload.model_dump())
@router.get("/auth/config", response_model=AuthConfig)
@@ -71,6 +49,8 @@ async def auth_config() -> AuthConfig:
return AuthConfig(
bot_username=settings.bot_username or "seoulmate_officialbot",
vapid_public_key=settings.vapid_public_key or None,
app_env=settings.app_env,
allow_dev_auth=settings.allow_dev_auth and not settings.is_production,
)
@@ -80,7 +60,7 @@ async def webapp_auth(
) -> User:
user_data = verify_webapp_init_data(payload.init_data, settings.bot_token)
telegram_id = int(user_data["id"])
return await upsert_telegram_user(
return await get_or_create_telegram_user(
session,
telegram_id=telegram_id,
username=user_data.get("username"),
@@ -96,7 +76,7 @@ async def telegram_login(
) -> User:
values = verify_login_widget(payload.model_dump(), settings.bot_token)
telegram_id = int(values["id"])
return await upsert_telegram_user(
return await get_or_create_telegram_user(
session,
telegram_id=telegram_id,
username=values.get("username"),
@@ -105,21 +85,35 @@ async def telegram_login(
)
@router.get("/me", response_model=UserRead)
async def current_user_profile(current_user: User = Depends(get_current_telegram_user)) -> User:
return current_user
@router.get("/telegram/{telegram_id}", response_model=UserRead)
async def get_user_by_telegram_id(
telegram_id: int, session: AsyncSession = Depends(get_session)
telegram_id: int,
session: AsyncSession = Depends(get_session),
x_internal_api_token: str | None = Header(default=None, alias="X-Internal-API-Token"),
) -> User:
require_internal_api_token(x_internal_api_token)
result = await session.execute(select(User).where(User.telegram_id == telegram_id))
return result.scalar_one()
user = result.scalar_one_or_none()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
@router.patch("/{user_id}/preferences", response_model=UserRead)
async def update_preferences(
user_id: int, payload: UserPreferencesUpdate, session: AsyncSession = Depends(get_session)
user_id: int,
payload: UserPreferencesUpdate,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> User:
user = await session.get(User, user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
if current_user.id != user_id:
raise HTTPException(status_code=403, detail="Forbidden")
user = current_user
for field, value in payload.model_dump(exclude_none=True).items():
setattr(user, field, value)
await session.commit()
@@ -133,10 +127,10 @@ async def save_push_subscription(
payload: PushSubscriptionCreate,
request: Request,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> None:
user = await session.get(User, user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
if current_user.id != user_id:
raise HTTPException(status_code=403, detail="Forbidden")
result = await session.execute(
select(PushSubscription).where(
PushSubscription.user_id == user_id,
@@ -166,8 +160,15 @@ async def save_push_subscription(
async def due_reminders(
user_id: int,
days: int = 30,
limit: int = 50,
offset: int = 0,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_telegram_user),
) -> list[ReminderRead]:
if current_user.id != user_id:
raise HTTPException(status_code=403, detail="Forbidden")
limit = min(max(limit, 1), 200)
offset = max(offset, 0)
today = date.today()
horizon = today + timedelta(days=max(1, min(days, 180)))
stmt = (
@@ -183,6 +184,8 @@ async def due_reminders(
)
)
.order_by(ServiceEntry.next_due_date.asc().nulls_last())
.limit(limit)
.offset(offset)
)
rows = (await session.execute(stmt)).all()
reminders: list[ReminderRead] = []