diff --git a/MEMORY.md b/MEMORY.md index 65e1892..e05345d 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -566,6 +566,7 @@ - 未登录访问后台时提示“前往登录”(`/login`); - 账号菜单提供“用户管理”(`/users`)作为默认后台入口。 - 退出登录口径:统一跳转到登录页 `/login`(不保留在当前后台路由)。 +- 宿主机 Nginx 的 `location = /fl` 不要再重定向到 `/fl/`;在 `basePath=/fl` 下,这会和 Next 的路径归一逻辑相互打架。当前口径是把 `/fl` 直接导向 `/fl/login`,并在改完 `quiz.conf` 后立即 `nginx -t && nginx -s reload`。 ## 站点标题口径(2026-04-24) diff --git a/api/app/api/v1/auth.py b/api/app/api/v1/auth.py index dd705fe..742f183 100644 --- a/api/app/api/v1/auth.py +++ b/api/app/api/v1/auth.py @@ -28,24 +28,37 @@ def _client_ip(request: Request) -> str | None: return None -def _set_refresh_cookie(response: Response, token: str) -> None: +def _is_secure_request(request: Request) -> bool: + forwarded_proto = request.headers.get("x-forwarded-proto") + if forwarded_proto: + return forwarded_proto.split(",")[0].strip().lower() == "https" + return request.url.scheme == "https" + + +def _refresh_cookie_secure(request: Request) -> bool: + if not settings.refresh_cookie_secure: + return False + return _is_secure_request(request) + + +def _set_refresh_cookie(request: Request, response: Response, token: str) -> None: response.set_cookie( key=settings.refresh_cookie_name, value=token, httponly=True, - secure=settings.refresh_cookie_secure, + secure=_refresh_cookie_secure(request), samesite=settings.refresh_cookie_samesite, max_age=settings.refresh_token_expire_days * 24 * 60 * 60, path="/api/v1/auth", ) -def _clear_refresh_cookie(response: Response) -> None: +def _clear_refresh_cookie(request: Request, response: Response) -> None: response.delete_cookie( key=settings.refresh_cookie_name, path="/api/v1/auth", httponly=True, - secure=settings.refresh_cookie_secure, + secure=_refresh_cookie_secure(request), samesite=settings.refresh_cookie_samesite, ) @@ -71,7 +84,7 @@ def register( user_agent=request.headers.get("user-agent"), ip_address=_client_ip(request), ) - _set_refresh_cookie(response, result.refresh_token) + _set_refresh_cookie(request, response, result.refresh_token) return _to_auth_response(result) @@ -88,7 +101,7 @@ def login( user_agent=request.headers.get("user-agent"), ip_address=_client_ip(request), ) - _set_refresh_cookie(response, result.refresh_token) + _set_refresh_cookie(request, response, result.refresh_token) return _to_auth_response(result) @@ -104,7 +117,7 @@ def refresh( user_agent=request.headers.get("user-agent"), ip_address=_client_ip(request), ) - _set_refresh_cookie(response, result.refresh_token) + _set_refresh_cookie(request, response, result.refresh_token) return _to_auth_response(result) @@ -119,7 +132,7 @@ def logout( request.cookies.get(settings.refresh_cookie_name), user_id=None, ) - _clear_refresh_cookie(response) + _clear_refresh_cookie(request, response) return MessageResponse(message="Logged out") diff --git a/memory/2026-05-12.md b/memory/2026-05-12.md new file mode 100644 index 0000000..2cd8f84 --- /dev/null +++ b/memory/2026-05-12.md @@ -0,0 +1,30 @@ +## Work Log - graphify 构建 api/app 后端知识图谱(2026-05-12) + +- 背景: + - 对 `./api/app` 执行 `$graphify`,目标是生成后端结构图谱,辅助后续代码导航与跨模块关系追踪。 + - 仓库根目录检测结果超过 graphify 建议阈值,因此改为对子目录 `api/app` 单独建图。 + +- 语料与执行结果: + - `api/app` 检测为纯代码语料:`110` 个代码文件,约 `60,168` 词。 + - 按 graphify 规则跳过 semantic extraction,仅执行 AST 抽取、聚类、报告与 HTML 可视化。 + - 产物位于仓库根目录 `graphify-out/`: + - `graph.html` + - `GRAPH_REPORT.md` + - `graph.json` + - `cost.json` + - `manifest.json` + +- 关键指标: + - 图规模:`1174` nodes / `3445` edges / `35` communities。 + - Token benchmark: + - 语料约 `80,224` naive tokens + - 平均查询成本约 `12,893` tokens + - 约 `6.2x` token reduction per query + +- 关注点: + - 主要桥接节点包括 `utcnow()`、`publish_topic()`、`Base`、`UserPublic`、`normalize_virtual_path()`。 + - 低内聚社区主要集中在 `Elevation Workflow`、`Lightning Analysis`,适合后续做依赖与职责拆分审视。 + +- 风险与影响: + - 本次未改业务代码,仅新增图谱输出与当天记忆文件。 + - `graphify-out/` 与 `api/app/graphify-out/cache/ast/` 为新生成工件,提交前应按需要决定是否纳入版本管理。 diff --git a/memory/2026-05-16.md b/memory/2026-05-16.md index 50d4444..97d3a72 100644 --- a/memory/2026-05-16.md +++ b/memory/2026-05-16.md @@ -128,3 +128,24 @@ - 风险与关注点: - 当前别名映射基于已知历史菜单路径补齐;若库里还存在其他非常规旧 path,仍需按实际反馈继续补别名表。 + +## Work Log - 生产入口 `/fl/login` 404 排查与 host Nginx 修复(2026-05-16) + +- 背景: + - 线上 `https://www.quizck.cn/fl/login` 一度返回 404,但仓库内 `web` 容器与本地代理均已包含 `/login` 页面。 + - 进一步比对发现,问题出在宿主机 Nginx 的 `/fl` 精确匹配:`location = /fl { return 301 /fl/; }` 会把入口导向 `/fl/`,而 Next 在 `/fl/` 上又会归一回 `/fl`,形成不必要的重定向回环/错路由链。 + +- 本次修复: + - `/etc/nginx/conf.d/quiz.conf` + - 将 `location = /fl` 的重定向目标改为 `/fl/login`。 + - 保留 `location ^~ /fl/` 继续反代到 `127.0.0.1:3000`。 + - 执行 `nginx -t && nginx -s reload` 让配置立即生效。 + +- 验证: + - `curl -I https://www.quizck.cn/fl/login` 返回 `200 OK`。 + - `curl -IL https://www.quizck.cn/fl` 返回 `301 -> /fl/login -> 200`。 + - `curl -IL https://www.quizck.cn/fl/` 返回 `308 -> /fl -> /fl/login -> 200`。 + +- 风险与关注点: + - `/fl/` 仍会多一次跳转,但不再出现 404。 + - 后续若统一外部入口,优先直接使用 `/fl/login`,避免再依赖 `/fl` 的归一逻辑。 diff --git a/web/src/middleware.ts b/web/src/middleware.ts index 2db5f35..ce99cf0 100644 --- a/web/src/middleware.ts +++ b/web/src/middleware.ts @@ -39,18 +39,6 @@ function stripBasePath(pathname: string): string { return pathname; } -function withBasePath(pathname: string): string { - if (!APP_BASE_PATH) { - return pathname; - } - - if (pathname === "/") { - return APP_BASE_PATH; - } - - return `${APP_BASE_PATH}${pathname}`; -} - function isBypassedPath(pathname: string): boolean { if (pathname === "/" || pathname === "/login" || pathname === "/login/") { return true; @@ -75,18 +63,18 @@ export function middleware(request: NextRequest) { // Keep backward compatibility for existing /admin links and legacy menu aliases. if (pathname.startsWith("/admin/")) { const url = request.nextUrl.clone(); - url.pathname = withBasePath(canonicalPath); + url.pathname = canonicalPath; return NextResponse.redirect(url); } if (canonicalPath !== pathname) { const url = request.nextUrl.clone(); - url.pathname = withBasePath(canonicalPath); + url.pathname = canonicalPath; return NextResponse.redirect(url); } // New public URLs without /admin are internally rewritten to existing routes. const url = request.nextUrl.clone(); - url.pathname = withBasePath(`/admin${canonicalPath}`); + url.pathname = `/admin${canonicalPath}`; return NextResponse.rewrite(url); }