diff --git a/CHANGELOG.md b/CHANGELOG.md index 721a128..b8f5e5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 4.9.8 - 2026-07-09 + +- 修复登录完成跳转后 Network 面板出现 `/api/user/config` 和 `/api/user/sync` 两个 400 请求的问题。 +- `AutoSync` 改为在 `PasswordGate` 确认服务端 session 并放行页面内容后再挂载,避免本地旧 session 早于服务端 cookie 校验触发云同步。 +- 用户配置和云同步 API 在未认证时返回 `401 Authentication required`,不再使用 `400 Missing profileId` 表达会话缺失。 +- 新增未认证响应回归测试,锁定受保护同步接口的状态码语义。 + ## 4.9.7 - 2026-07-09 - 修复 Android TV / 小米电视旧 WebView 83 打开站点后白屏的问题。 diff --git a/app-release.json b/app-release.json index 7d8d7ba..06a155a 100644 --- a/app-release.json +++ b/app-release.json @@ -4,8 +4,18 @@ "name": "KVideo", "branch": "main" }, - "currentVersion": "4.9.7", + "currentVersion": "4.9.8", "releases": [ + { + "version": "4.9.8", + "publishedAt": "2026-07-09", + "title": "修复登录后同步接口 400", + "notes": [ + "自动同步组件现在只会在访问密码 gate 确认服务端会话并完成本地 session 镜像后挂载,避免登录跳转阶段提前请求 /api/user/config 与 /api/user/sync。", + "用户配置和云同步 API 在没有有效服务端 session 时改为返回 401 Authentication required,不再把未认证状态伪装成 Missing profileId 的 400。", + "新增未认证响应回归测试,防止受保护同步接口的状态码语义再次退化。" + ] + }, { "version": "4.9.7", "publishedAt": "2026-07-09", diff --git a/app/api/user/config/route.ts b/app/api/user/config/route.ts index bc0f4b4..3195d41 100644 --- a/app/api/user/config/route.ts +++ b/app/api/user/config/route.ts @@ -7,6 +7,7 @@ import { Redis } from '@upstash/redis'; import { NextRequest, NextResponse } from 'next/server'; +import { authenticationRequiredResponse } from '@/lib/server/api-responses'; import { getServerSession } from '@/lib/server/auth'; export const runtime = 'edge'; @@ -23,7 +24,7 @@ export async function GET(request: NextRequest) { const profileId = session?.profileId; if (!profileId) { - return NextResponse.json({ error: 'Missing profileId' }, { status: 400 }); + return authenticationRequiredResponse(); } try { @@ -43,7 +44,7 @@ export async function POST(request: NextRequest) { const profileId = session?.profileId; if (!profileId) { - return NextResponse.json({ error: 'Missing profileId' }, { status: 400 }); + return authenticationRequiredResponse(); } try { diff --git a/app/api/user/sync/route.ts b/app/api/user/sync/route.ts index 716ea1f..cd190d6 100644 --- a/app/api/user/sync/route.ts +++ b/app/api/user/sync/route.ts @@ -1,5 +1,6 @@ import { Redis } from '@upstash/redis'; import { NextRequest, NextResponse } from 'next/server'; +import { authenticationRequiredResponse } from '@/lib/server/api-responses'; import { getServerSession } from '@/lib/server/auth'; // 确保这行代码在整个文件中只出现一次 @@ -12,7 +13,7 @@ export async function GET(request: NextRequest) { const profileId = session?.profileId; if (!profileId) { - return NextResponse.json({ error: 'Missing profileId' }, { status: 400 }); + return authenticationRequiredResponse(); } try { @@ -32,7 +33,7 @@ export async function POST(request: NextRequest) { const profileId = session?.profileId; if (!profileId) { - return NextResponse.json({ error: 'Missing profileId' }, { status: 400 }); + return authenticationRequiredResponse(); } try { diff --git a/app/layout.tsx b/app/layout.tsx index 68a1c38..565b6bf 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -116,8 +116,6 @@ export default async function RootLayout({ scriptUrl={videoTogetherScriptUrl} settingUrl={videoTogetherSettingUrl} /> - {/* 加入自动同步组件,它会在后台默默工作,我们放在 ThemeProvider 内部的最前面 */} - @@ -132,6 +130,7 @@ export default async function RootLayout({ process.env.UPSTASH_REDIS_REST_TOKEN ) )}> + {children} diff --git a/components/AutoSync.tsx b/components/AutoSync.tsx index e2bb6e0..b56659f 100644 --- a/components/AutoSync.tsx +++ b/components/AutoSync.tsx @@ -8,10 +8,10 @@ import { useConfigSync } from '@/lib/hooks/useConfigSync'; import { getSession } from '@/lib/store/auth-store'; // 防抖函数,防止频繁请求 -function debounce(fn: Function, delay: number) { - let timeoutId: NodeJS.Timeout; - return (...args: any[]) => { - clearTimeout(timeoutId); +function debounce(fn: (...args: Args) => void, delay: number) { + let timeoutId: ReturnType | undefined; + return (...args: Args) => { + if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; } diff --git a/lib/server/api-responses.ts b/lib/server/api-responses.ts new file mode 100644 index 0000000..81f55aa --- /dev/null +++ b/lib/server/api-responses.ts @@ -0,0 +1,5 @@ +import { NextResponse } from 'next/server'; + +export function authenticationRequiredResponse(): NextResponse { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); +} diff --git a/package-lock.json b/package-lock.json index 5156995..0d7f834 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kvideo", - "version": "4.9.7", + "version": "4.9.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "kvideo", - "version": "4.9.7", + "version": "4.9.8", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/package.json b/package.json index 31b16eb..bf04421 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kvideo", - "version": "4.9.7", + "version": "4.9.8", "private": true, "scripts": { "dev": "node scripts/next-with-lan-access.mjs dev", diff --git a/tests/api-responses.test.ts b/tests/api-responses.test.ts new file mode 100644 index 0000000..e794aaa --- /dev/null +++ b/tests/api-responses.test.ts @@ -0,0 +1,12 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { authenticationRequiredResponse } from '@/lib/server/api-responses'; + +test('authenticationRequiredResponse returns a 401 JSON response', async () => { + const response = authenticationRequiredResponse(); + + assert.equal(response.status, 401); + assert.equal(response.headers.get('content-type')?.includes('application/json'), true); + assert.deepEqual(await response.json(), { error: 'Authentication required' }); +});