From 18c77296c20a29f94be518ae962ae254ce3cf519 Mon Sep 17 00:00:00 2001 From: snowy-yang <102860110+snowy-yang@users.noreply.github.com> Date: Sat, 26 Sep 2026 09:51:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(routes):=20=E8=BF=81=E7=A7=BB=E9=A6=96?= =?UTF-8?q?=E9=A1=B5/=E9=9D=99=E6=80=81=E8=B5=84=E6=BA=90=20+=20/auth/me?= =?UTF-8?q?=20=E8=AE=A4=E8=AF=81=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - web/routers/index.py:GET / 首页,用 Jinja2 以 Flask 同口径渲染 (keep_trailing_newline=False)——输出与 Flask render_template 逐字节一致 - web/app.py:挂载 /static 静态资源(site_auth.js 等,与 Flask 同目录) - web/routers/auth_status.py:/api/v1/auth/me 认证状态 · user/duration_text/announcement/smtp_configured/register_credits 逐字段对齐 - core/repo.py:补 get_announcement / get_smtp_config / smtp_configured 异步读 契约奇偶校验:28 项全 OK(新增 index 逐字节、auth:me); 验证:pytest 51/51;ruff All checks passed;pyright 0 errors; uvicorn 实跑 / 与 /static 与 /api/v1/* 均 200。 --- core/repo.py | 17 +++++++++ web/app.py | 12 ++++++ web/routers/auth_status.py | 78 ++++++++++++++++++++++++++++++++++++++ web/routers/index.py | 37 ++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 web/routers/auth_status.py create mode 100644 web/routers/index.py diff --git a/core/repo.py b/core/repo.py index 655f27f..a396195 100644 --- a/core/repo.py +++ b/core/repo.py @@ -59,6 +59,23 @@ async def get_user_by_id(uid: int) -> dict[str, Any] | None: return dict(row) if row is not None else None +# ---------- 公告 / SMTP 配置(读) ---------- + +async def get_announcement() -> dict[str, Any]: + row = await db.fetch_one('SELECT * FROM announcements WHERE id = 1') + return dict(row) if row else {'title': '', 'content': '', 'popup_mode': 'off'} + + +async def get_smtp_config() -> dict[str, Any]: + row = await db.fetch_one('SELECT * FROM smtp_config WHERE id = 1') + return dict(row) if row else {} + + +async def smtp_configured() -> bool: + cfg = await get_smtp_config() + return bool(cfg.get('host') and cfg.get('sender') and cfg.get('password')) + + # ---------- billing ledger(只读对账) ---------- async def ledger_open_count() -> int: diff --git a/web/app.py b/web/app.py index 7dcad78..d8a2c63 100644 --- a/web/app.py +++ b/web/app.py @@ -14,15 +14,19 @@ """ from contextlib import asynccontextmanager +from pathlib import Path from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles from core.config import APP_VERSION from core.security import init_secret_key from web.errors import install_error_handlers from web.middleware import ResponseHygieneMiddleware from web.routers import account as account_router +from web.routers import auth_status as auth_status_router from web.routers import delivery as delivery_router +from web.routers import index as index_router from web.routers import music as music_router from web.routers import podcast as podcast_router from web.routers import site as site_router @@ -70,6 +74,11 @@ def create_app(serve_flask_fallback: bool = False) -> FastAPI: # 响应侧统一处理:安全头 + 音频直链签名保护 + 版本指纹抹除 app.add_middleware(ResponseHygieneMiddleware) + # 静态资源(/static/site_auth.js 等)——与 Flask static_folder 同目录 + _static_dir = Path(__file__).resolve().parent.parent / 'static' + if _static_dir.is_dir(): + app.mount('/static', StaticFiles(directory=str(_static_dir)), name='static') + # 已迁移路由:统一挂 /api/v1(迁移期与 Flask 根路径并存,便于对比) app.include_router(system_router.router, prefix='/api/v1', tags=['system']) app.include_router(site_router.router, prefix='/api/v1', tags=['site']) @@ -79,6 +88,9 @@ def create_app(serve_flask_fallback: bool = False) -> FastAPI: app.include_router(tasks_router.router, prefix='/api/v1', tags=['tasks']) app.include_router(delivery_router.router, prefix='/api/v1', tags=['delivery']) app.include_router(stream_router.router, prefix='/api/v1', tags=['stream']) + app.include_router(auth_status_router.router, prefix='/api/v1', tags=['auth']) + # 首页与静态资源(迁移完成形态;fallback 模式下同路径也能由 Flask 提供) + app.include_router(index_router.router, tags=['index']) install_error_handlers(app) diff --git a/web/routers/auth_status.py b/web/routers/auth_status.py new file mode 100644 index 0000000..02cab50 --- /dev/null +++ b/web/routers/auth_status.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +"""认证状态端点(FastAPI 栈)——与 Flask /auth/me 响应契约逐字段对齐。 + +- GET/POST /api/v1/auth/me ↔ Flask /auth/me + · user:站点用户公开字段(未登录为 None) + · duration_text:次数文案(不限次数 / 已用完 / N 次) + · announcement、smtp_configured、register_credits 逐字段一致 +""" +from typing import Annotated + +from fastapi import APIRouter, Depends, Request + +from core import repo +from web import ratelimit +from web.deps import SiteUser, current_user +from web.response import error, success + +router = APIRouter() + + +def _format_credits(count: int) -> str: + if count < 0: + return '不限次数' + if count <= 0: + return '已用完' + return f'{count} 次' + + +def _user_public(row: dict) -> dict: + return { + 'id': row['id'], + 'username': row['username'], + 'email': row.get('email'), + 'role': row['role'], + 'duration_seconds': row['duration_seconds'], + 'created_at': row.get('created_at'), + 'last_login': row.get('last_login'), + 'ann_seen': row.get('ann_seen'), + } + + +@router.api_route('/auth/me', methods=['GET', 'POST']) +async def auth_me( + user: Annotated[SiteUser | None, Depends(current_user)], + request: Request, +): + """当前会话与站点公开配置(未登录也返回,与 Flask 同口径)。""" + await ratelimit.guard(request, bucket='free', limit=30, window=60) + try: + ann = await repo.get_announcement() + smtp_ok = await repo.smtp_configured() + try: + credits = int(await repo.get_setting('register_credits', '3') or 0) + except (TypeError, ValueError): + credits = 3 + credits = max(0, credits) + + pub = None + duration_text = '' + if user is not None: + row = await repo.get_user_by_id(user.id) + if row is not None: + pub = _user_public(row) + duration_text = _format_credits(int(row.get('duration_seconds') or 0)) + return success({ + 'user': pub, + 'duration_text': duration_text, + 'announcement': { + 'title': ann.get('title', ''), + 'content': ann.get('content', ''), + 'popup_mode': ann.get('popup_mode', 'off'), + 'updated_at': ann.get('updated_at', ''), + }, + 'smtp_configured': smtp_ok, + 'register_credits': credits, + }) + except Exception: + return error('服务暂时不可用,请稍后再试', 500) diff --git a/web/routers/index.py b/web/routers/index.py new file mode 100644 index 0000000..914ca6c --- /dev/null +++ b/web/routers/index.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +"""首页与静态资源(FastAPI 栈)。 + +- GET / 首页:渲染同一份 templates/index.html,禁止缓存(与 Flask 一致) +- 静态资源由 Starlette StaticFiles 挂载 /static(见 web/app.py) + +说明:首页在 Flask fallback 模式下由 Flask 兜底提供;本端点用于「关闭 Flask 后」 +FastAPI 独立提供首页(迁移完成的形态)。两者渲染的是同一模板,故内容一致。 +""" +from pathlib import Path + +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse + +router = APIRouter() + +_TEMPLATE = Path(__file__).resolve().parent.parent.parent / 'templates' / 'index.html' + + +@router.get('/', response_class=HTMLResponse) +async def index(request: Request) -> HTMLResponse: + """首页(禁止缓存,确保前端更新即时生效)。 + + 用 Jinja2 以 Flask 相同的默认口径渲染(keep_trailing_newline=False), + 使输出与 Flask render_template 逐字节一致。 + """ + import jinja2 + + env = jinja2.Environment( + loader=jinja2.FileSystemLoader(str(_TEMPLATE.parent)), + autoescape=True, + ) + html = env.get_template(_TEMPLATE.name).render() + return HTMLResponse( + content=html, + headers={'Cache-Control': 'no-cache, no-store, must-revalidate'}, + )