From 26827e31d536b22d71a1584a4fda65d296f63f67 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 26 Oct 2026 19:10:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=99=BB=E5=BD=95=E6=94=B9=E7=94=A8=20?= =?UTF-8?q?JWT=EF=BC=88access=20token=20+=20refresh=20token=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=86=8D=E7=94=A8=20cookie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 登录返回 15 分钟的 access token(HS256,密钥 JWT_SECRET,未配置则每次启动随机) 和 14 天的 refresh token;请求带 Authorization: Bearer,WebSocket 用 ?token= - refresh token 只存 sha256,放在新表 auth_refresh_token(删除旧的 auth_session); POST /account/refresh 换新的 access token,退出、改密码、封号时作废 - 带了无效或过期的 token 一律 401,前端据此刷新:快过期时提前刷新、并发请求只刷新一次, 401 时刷新后重试一次,refresh token 也失效才退出登录 - 不再用 cookie,去掉 CORS credentials 和写请求的 Origin 校验 Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 9 +-- client/src/App.vue | 13 +--- client/src/api.ts | 40 +++++++++--- client/src/router.ts | 5 +- client/src/store/guidance.ts | 23 +++++-- client/src/store/prompt.ts | 13 +++- client/src/token.ts | 70 ++++++++++++++++++++ client/src/utils/const.ts | 3 +- server/.env.example | 2 + server/scripts/create-superuser.ts | 2 +- server/scripts/migrate-from-postgres.ts | 2 +- server/src/app.ts | 21 ++---- server/src/auth.ts | 87 +++++++++++++++---------- server/src/config.ts | 7 +- server/src/db.ts | 6 +- server/src/index.ts | 4 +- server/src/origin.ts | 12 ---- server/src/routes/account.ts | 22 ++++--- server/src/ws.ts | 15 ++--- server/tests/api.test.ts | 54 ++++++++++++--- server/tests/helpers.ts | 12 ++-- 21 files changed, 281 insertions(+), 141 deletions(-) create mode 100644 client/src/token.ts delete mode 100644 server/src/origin.ts diff --git a/CLAUDE.md b/CLAUDE.md index ef8ee41..8fb5f1a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,19 +12,20 @@ The backend was rewritten from Django in 2026-10 to cut memory use (~25 MB vs. 1 - **`server/`** — Bun + **Hono** HTTP API, Bun's native WebSocket, **`bun:sqlite`** (single SQLite file, WAL). No Redis, no Postgres. - `src/index.ts` — `Bun.serve`: WebSocket upgrades go to `ws.ts`, everything else to the Hono app - - `src/app.ts` — mounts routers under `/api/`, CORS, Origin check on writes, session middleware, error → `{ detail }` JSON (ninja-compatible; validation errors are 422) + - `src/app.ts` — mounts routers under `/api/`, CORS, auth middleware, error → `{ detail }` JSON (ninja-compatible; validation errors are 422) - `src/db.ts` — schema (`CREATE TABLE IF NOT EXISTS`, same DDL as Django), `all/get/run/tx` helpers using `$name` params, time (`now()`, `iso()`) and UUID (`uuidHex()`, `uuidOut()`) conversions - - `src/auth.ts` — Django-compatible password hashing, sessions in table `auth_session` (cookie `sessionid`), `requireUser/requireAdmin/requireSuper` (401 / 403) + - `src/auth.ts` — Django-compatible password hashing; JWT login, no cookies: `POST /account/login` returns a 15-min HS256 `access_token` (sent as `Authorization: Bearer`, signed with `JWT_SECRET`, random per process if unset) and a 14-day opaque `refresh_token` (only its sha256 is stored, in `auth_refresh_token`; `POST /account/refresh` trades it for a new access token, logout / password change / deactivation delete it). An invalid or expired Bearer token is a 401 even on public endpoints so the client knows to refresh. `requireUser/requireAdmin/requireSuper` (401 / 403) - `src/routes/` — `account`, `tasks` (tutorial / challenge / creation), `files` (task assets + image upload), `submission`, `showcase` (mounted under `/submission/showcase`), `prompt` - `src/services/` — `score.ts` (rating weights super×0.5, admin×0.3, normal×0.2, then Bayesian smoothing with C=3 and zones), `gradebook.ts`, `llm.ts` (OpenAI-compatible streaming via fetch; DeepSeek default, ARK for doubao models), `classifier.ts` (Bloom-level L1–L6 prompt tagging, runs in background), `cascade.ts` (Django's CASCADE deletes, done by hand), `pyround.ts` (`pyRound`/`pySum` reproduce Python's `round()` and `sum()` so scores match the old system bit-for-bit) - - `src/ws.ts` — `/ws/prompt//` (code generation chat) and `/ws/guidance//` (prompt coaching); messages per connection are processed serially; on disconnect the pending user message is deleted and the LLM request aborted + - `src/ws.ts` — `/ws/prompt//?token=` (code generation chat) and `/ws/guidance//?token=...` (prompt coaching); messages per connection are processed serially; on disconnect the pending user message is deleted and the LLM request aborted - `scripts/` — one-off CLIs: `migrate-from-postgres.ts`, `create-superuser.ts`, `classify-prompts.ts`, `recalculate-scores.ts` - Roles: `super`, `admin`, `normal`. Task model is multi-table: `task_task` + one of `task_tutorial` / `task_challenge` / `task_creation` (`task_ptr_id`) - **`client/`** — Vue 3 + TypeScript SPA built with **Vite**; Bun is the package manager and runtime (no Node/npm) - `vite.config.ts` — browser targets are Chrome ≥ 87 (lab machines run Chrome 105); `@vitejs/plugin-legacy` only injects usage-based core-js polyfills, no legacy chunks - UI framework: **Naive UI** (auto-imported via `unplugin-vue-components`) - - `src/api.ts` — Axios-based API client; 401 clears login and goes home, 403 alerts and goes home + - `src/token.ts` — tokens in localStorage; refreshes the access token shortly before expiry (one request at a time) and builds WebSocket URLs + - `src/api.ts` — Axios-based API client; adds the Bearer header, on 401 refreshes once and retries, otherwise clears login and goes home; 403 alerts and goes home - `src/router.ts` — Vue Router with auth guards - `src/store/` — Reactive state (plain Vue refs, not Vuex/Pinia); `prompt.ts` / `guidance.ts` own the WebSockets - `src/pages/`, `src/components/` — user management uses `components/dashboard/UserFormModal.vue` (there is no Django Admin anymore) diff --git a/client/src/App.vue b/client/src/App.vue index fdf20dd..5ee13f3 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -2,10 +2,9 @@ import { dateZhCN, zhCN } from "naive-ui" import type { GlobalThemeOverrides } from "naive-ui" import Login from "./components/Login.vue" -import { onMounted, watch } from "vue" +import { onMounted } from "vue" import { Account } from "./api" -import { authed, user } from "./store/user" -import { STORAGE_KEY } from "./utils/const" +import { user } from "./store/user" import hljs from "highlight.js/lib/core" const themeOverrides: GlobalThemeOverrides = { @@ -28,14 +27,6 @@ onMounted(async () => { user.loaded = true } }) - -watch(authed, (v) => { - if (v) { - localStorage.setItem(STORAGE_KEY.LOGIN, "true") - } else { - localStorage.removeItem(STORAGE_KEY.LOGIN) - } -})