thermograph/backend/notifications/discord_link.py

92 lines
3.1 KiB
Python
Raw Normal View History

"""Discord's legacy /discord/* auth routes, plus the DM opt-in toggle.
The OAuth flow itself moved to ``accounts/oauth.py`` when Google was added this
module holds no auth logic, only the paths that have to keep answering:
* **/discord/link/callback is registered in Discord's developer portal.** Changing
it would need an operator to edit the portal before any Discord login worked, so
it stays exactly where it is and forwards to the shared handler.
* The other /discord/* routes are what the currently-deployed frontend calls.
Frontend and backend deploy independently (see the root CLAUDE.md), so a newer
backend must keep serving an older frontend. They can go once no deployed
frontend predates /oauth/*.
``POST /discord/dm`` is not legacy and is not moving: it toggles alert delivery,
which is a notifications concern, not an authentication one.
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, Request
from pydantic import BaseModel
from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
from accounts import oauth
Split the backend into domain packages (#217) * Centralize filesystem paths in a single module Add paths.py, which resolves the repo root once and derives the cache, accounts DB, logs, templates, frontend and bundled-city-data locations from it. Replace the 13 per-module `dirname(__file__)/..` anchors with references to it, so a module's location no longer determines where the app reads its data. Env overrides (accounts DB, VAPID, IndexNow) are unchanged; every resolved path is byte-identical to before. Groundwork for moving modules into packages without re-pointing paths. Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62 * Split the backend into domain packages Group the flat backend modules into packages that mirror their concerns: data/ climate, grading, scoring, grid, places, cities, city_events, store web/ app, views, homepage, content, schemas notifications/ notify, digest, push, mailer, discord, discord_interactions, discord_link accounts/ models, users, api_accounts, db core/ metrics, singleton, audit Intra-project imports are rewritten to the package-qualified form. The entry scripts (indexnow, warm_cities, migrate, gen_cities, gen_flavor) and paths.py stay at the backend/ root, and backend/app.py becomes a shim re-exporting web.app:app so the launch target stays `app:app` — run.sh, the systemd units, and CI need no change. Verified: full suite (318) passes, `uvicorn app:app` boots and serves the home/SEO/static/API surfaces, and every root script imports clean. Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62
2026-07-20 05:31:03 +00:00
from accounts.db import get_async_session
from accounts.models import User
from accounts.users import (
current_active_user,
current_user_optional,
get_access_token_db,
get_user_manager,
)
router = APIRouter(tags=["discord"])
@router.get("/discord/config")
async def discord_config():
"""Superseded by /oauth/config, which reports every provider. Kept because the
deployed frontend asks this one."""
return {"enabled": oauth.enabled("discord")}
@router.get("/discord/link/start")
async def link_start(request: Request, user=Depends(current_active_user)):
return await oauth.link_start("discord", request, user)
@router.get("/discord/login/start")
async def login_start(request: Request, user=Depends(current_user_optional)):
return await oauth.login_start("discord", request, user)
@router.get("/discord/link/callback")
async def link_callback(
request: Request,
user=Depends(current_user_optional),
session: AsyncSession = Depends(get_async_session),
user_manager=Depends(get_user_manager),
access_token_db=Depends(get_access_token_db),
):
"""The URL registered in Discord's portal. Both Discord flows land here."""
return await oauth._callback("discord", request, user, session, user_manager,
access_token_db)
@router.post("/discord/unlink", status_code=204)
async def unlink(
user=Depends(current_active_user),
session: AsyncSession = Depends(get_async_session),
):
await oauth._unlink("discord", user, session)
class DmToggle(BaseModel):
enabled: bool
@router.post("/discord/dm", status_code=204)
async def set_dm(
payload: DmToggle,
user=Depends(current_active_user),
session: AsyncSession = Depends(get_async_session),
):
"""Turn Discord DM alerts on or off without unlinking. A no-op unless a Discord
account is actually linked."""
await session.execute(
update(User).where(User.id == user.id, User.discord_id.is_not(None))
.values(discord_dm=payload.enabled)
)
await session.commit()