2026-07-27 00:56:43 +00:00
|
|
|
"""Discord's legacy /discord/* auth routes, plus the DM opt-in toggle.
|
2026-07-20 02:26:33 +00:00
|
|
|
|
2026-07-27 00:56:43 +00:00
|
|
|
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:
|
2026-07-20 02:26:33 +00:00
|
|
|
|
2026-07-27 00:56:43 +00:00
|
|
|
* **/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/*.
|
2026-07-26 18:16:55 +00:00
|
|
|
|
2026-07-27 00:56:43 +00:00
|
|
|
``POST /discord/dm`` is not legacy and is not moving: it toggles alert delivery,
|
|
|
|
|
which is a notifications concern, not an authentication one.
|
2026-07-20 02:26:33 +00:00
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-07-27 00:56:43 +00:00
|
|
|
from fastapi import APIRouter, Depends, Request
|
2026-07-20 04:04:45 +00:00
|
|
|
from pydantic import BaseModel
|
2026-07-27 00:56:43 +00:00
|
|
|
from sqlalchemy import update
|
2026-07-20 02:26:33 +00:00
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
2026-07-27 00:56:43 +00:00
|
|
|
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
|
2026-07-26 18:16:55 +00:00
|
|
|
from accounts.users import (
|
|
|
|
|
current_active_user,
|
|
|
|
|
current_user_optional,
|
|
|
|
|
get_access_token_db,
|
|
|
|
|
get_user_manager,
|
|
|
|
|
)
|
2026-07-20 02:26:33 +00:00
|
|
|
|
|
|
|
|
router = APIRouter(tags=["discord"])
|
|
|
|
|
|
2026-07-26 18:16:55 +00:00
|
|
|
|
2026-07-20 04:36:44 +00:00
|
|
|
@router.get("/discord/config")
|
|
|
|
|
async def discord_config():
|
2026-07-27 00:56:43 +00:00
|
|
|
"""Superseded by /oauth/config, which reports every provider. Kept because the
|
|
|
|
|
deployed frontend asks this one."""
|
|
|
|
|
return {"enabled": oauth.enabled("discord")}
|
2026-07-20 04:36:44 +00:00
|
|
|
|
|
|
|
|
|
2026-07-20 02:26:33 +00:00
|
|
|
@router.get("/discord/link/start")
|
|
|
|
|
async def link_start(request: Request, user=Depends(current_active_user)):
|
2026-07-27 00:56:43 +00:00
|
|
|
return await oauth.link_start("discord", request, user)
|
2026-07-26 18:16:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/discord/login/start")
|
|
|
|
|
async def login_start(request: Request, user=Depends(current_user_optional)):
|
2026-07-27 00:56:43 +00:00
|
|
|
return await oauth.login_start("discord", request, user)
|
2026-07-20 02:26:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/discord/link/callback")
|
|
|
|
|
async def link_callback(
|
|
|
|
|
request: Request,
|
2026-07-26 18:16:55 +00:00
|
|
|
user=Depends(current_user_optional),
|
2026-07-20 02:26:33 +00:00
|
|
|
session: AsyncSession = Depends(get_async_session),
|
2026-07-26 18:16:55 +00:00
|
|
|
user_manager=Depends(get_user_manager),
|
|
|
|
|
access_token_db=Depends(get_access_token_db),
|
2026-07-20 02:26:33 +00:00
|
|
|
):
|
2026-07-27 00:56:43 +00:00
|
|
|
"""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)
|
2026-07-26 18:16:55 +00:00
|
|
|
|
|
|
|
|
|
2026-07-20 02:26:33 +00:00
|
|
|
@router.post("/discord/unlink", status_code=204)
|
|
|
|
|
async def unlink(
|
|
|
|
|
user=Depends(current_active_user),
|
|
|
|
|
session: AsyncSession = Depends(get_async_session),
|
|
|
|
|
):
|
2026-07-27 00:56:43 +00:00
|
|
|
await oauth._unlink("discord", user, session)
|
2026-07-20 04:04:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-07-20 02:26:33 +00:00
|
|
|
)
|
|
|
|
|
await session.commit()
|