feat: persist map tile settings (provider/style/API keys) server-side on user record; unbake Thunderforest default key into backend config (THUNDERFOREST_DEFAULT_KEY) served via profile
This commit is contained in:
@@ -7,6 +7,7 @@ from datetime import datetime, date, timezone
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user, hash_password, verify_password
|
||||
from app.core.config import settings
|
||||
from app.models.user import User, WeightLog
|
||||
|
||||
router = APIRouter()
|
||||
@@ -36,6 +37,8 @@ class ProfileOut(BaseModel):
|
||||
estimated_max_hr: Optional[int]
|
||||
is_admin: bool
|
||||
dashboard_layout: Optional[list] = None
|
||||
map_settings: Optional[dict] = None
|
||||
thunderforest_default_key: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -45,6 +48,12 @@ class DashboardLayoutIn(BaseModel):
|
||||
layout: Optional[list] = None # react-grid-layout array of {i,x,y,w,h}
|
||||
|
||||
|
||||
class MapSettingsIn(BaseModel):
|
||||
provider: Optional[str] = None
|
||||
style: Optional[str] = None
|
||||
keys: Optional[dict] = None # {thunderforest, maptiler, ...} public tile keys
|
||||
|
||||
|
||||
def _estimated_max_hr(user: User) -> Optional[int]:
|
||||
if user.birth_year:
|
||||
return 220 - (datetime.now().year - user.birth_year)
|
||||
@@ -55,7 +64,27 @@ def _estimated_max_hr(user: User) -> Optional[int]:
|
||||
async def get_profile(current_user: User = Depends(get_current_user)):
|
||||
return {**{c.name: getattr(current_user, c.name)
|
||||
for c in User.__table__.columns},
|
||||
"estimated_max_hr": _estimated_max_hr(current_user)}
|
||||
"estimated_max_hr": _estimated_max_hr(current_user),
|
||||
"thunderforest_default_key": settings.thunderforest_default_key}
|
||||
|
||||
|
||||
@router.put("/map-settings")
|
||||
async def save_map_settings(
|
||||
body: MapSettingsIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Persist the user's global map tile preference (provider/style/keys)."""
|
||||
keys = {}
|
||||
for k, v in (body.keys or {}).items():
|
||||
keys[str(k)] = (v or "").strip()
|
||||
current_user.map_settings = {
|
||||
"provider": body.provider,
|
||||
"style": body.style,
|
||||
"keys": keys,
|
||||
}
|
||||
await db.commit()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.put("/dashboard-layout")
|
||||
@@ -111,7 +140,8 @@ async def update_profile(
|
||||
|
||||
return {**{c.name: getattr(current_user, c.name)
|
||||
for c in User.__table__.columns},
|
||||
"estimated_max_hr": _estimated_max_hr(current_user)}
|
||||
"estimated_max_hr": _estimated_max_hr(current_user),
|
||||
"thunderforest_default_key": settings.thunderforest_default_key}
|
||||
|
||||
|
||||
# ── Password change ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -28,6 +28,11 @@ class Settings(BaseSettings):
|
||||
# The Authorization Callback Domain there must match BASE_URL's host.
|
||||
strava_client_id: Optional[str] = Field(None, env="STRAVA_CLIENT_ID")
|
||||
strava_client_secret: Optional[str] = Field(None, env="STRAVA_CLIENT_SECRET")
|
||||
# Default Thunderforest tile API key, served to clients that haven't set
|
||||
# their own (a public, client-side tile key). Override per-deployment.
|
||||
thunderforest_default_key: str = Field(
|
||||
"872984f587484873a74ea454662ffacb", env="THUNDERFOREST_DEFAULT_KEY"
|
||||
)
|
||||
# Files
|
||||
file_store_path: str = Field("/data/files", env="FILE_STORE_PATH")
|
||||
# Environment
|
||||
|
||||
@@ -118,6 +118,15 @@ async def init_db():
|
||||
except Exception as e:
|
||||
print(f"users.dashboard_layout column migration skipped: {e}")
|
||||
|
||||
# map_settings column on users added after initial creation
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE users ADD COLUMN IF NOT EXISTS map_settings JSON"
|
||||
))
|
||||
except Exception as e:
|
||||
print(f"users.map_settings column migration skipped: {e}")
|
||||
|
||||
# Backfill avg_hr_day / max_hr_day from intraday_hr for Garmin Connect synced days
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
|
||||
@@ -40,6 +40,10 @@ class User(Base):
|
||||
# Saved dashboard widget layout (react-grid-layout array). Null = use default.
|
||||
dashboard_layout = Column(JSON, nullable=True)
|
||||
|
||||
# Global map tile preference: {provider, style, keys:{thunderforest, maptiler}}.
|
||||
# Null = use defaults. Tile API keys are public client-side keys.
|
||||
map_settings = Column(JSON, nullable=True)
|
||||
|
||||
activities = relationship("Activity", back_populates="user", cascade="all, delete-orphan")
|
||||
health_metrics = relationship("HealthMetric", back_populates="user", cascade="all, delete-orphan")
|
||||
named_routes = relationship("NamedRoute", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
Reference in New Issue
Block a user