From 2ad0472578f1b79806bc98ce9c091b9c542be43e Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Tue, 1 Sep 2026 12:23:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(fam-ui):=20=E6=9C=AA=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B7=B3=E8=BD=AC=20OIDC=20+=20=E7=99=BB?= =?UTF-8?q?=E5=BD=95/=E9=80=80=E5=87=BA=E5=85=A5=E5=8F=A3=EF=BC=8C?= =?UTF-8?q?=E6=89=93=E9=80=9A=20smart-camera=20=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROGRESS.md | 19 ++++++++++++++++++ fam-ui/src/App.vue | 49 ++++++++++++++++++++++++++++++++++++++++++---- fam-ui/src/api.js | 13 ++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 1b14e52..38ba586 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -522,3 +522,22 @@ AI 分析瓶颈: 帧5耗时 229s (疑似 ARM CPU 热降频), 其余帧 50-65s 2. `.env` 里 `AUTH_HUB_*` 是裸赋值(无 `export`),`source` 后不进环境,gunicorn 子进程读不到 → 登录一直 503 fail-closed(与之前“生产未部署”一致)。脚本加 `set -a` 包裹 source 修复。 **验证**:`smart-camera.zichuan.xyz/` 200、`/login` 302→auth.zichuan.xyz(redirect_uri=smart-camera)、`/api/*` 401;`oracle.zichuan.xyz/` 404、`/fam` 200;`api.zichuan.xyz` 已不通。fam-core 经修复脚本重启后 health ok。 + +## SPA 登录打通:未登录自动跳转 OIDC(2026-09-01) + +**问题**:域名收口后访问 `https://smart-camera.zichuan.xyz/timeline` 显示「⚠ 未登录 / 登陆不了啊」。后端 `/login`(302→auth-hub)与 Caddy `/login` 反代此前已验证可用,根因在**前端零登录逻辑**:`fam-ui` 调 `/api/*` 拿到 401 后只在页面上渲染字面错误 `data.error`("未登录"),从不发起 OIDC 跳转,也没有任何登录入口。 + +**改动(本仓库 `fam-ui/`)**: +- `src/api.js` `request()`:捕获 401 时 `window.location.href = '/login'` 触发统一登录(后端经 auth-hub 走 Authorization Code + PKCE);用模块级 `_redirectingToLogin` 开关保证单次会话只跳一次,并 `return new Promise(()=>{})` 阻止调用方继续渲染错误态;`/api/auth/check` 等白名单接口不会 401,不受影响。 +- `src/api.js` `api` 对象新增 `authCheck: () => request('/api/auth/check')`。 +- `src/App.vue`:`onMounted` 调 `api.authCheck()` 维护 `authed` 状态;左侧栏 + 移动端顶栏新增「🔑 登录」入口(``)与「👋 退出登录」按钮(`POST /api/logout` 后回 `/` 由后端 401 自动跳登录)。 + +**部署**:`npm run build` → `dist/` 经 `tar | ssh ubuntu@129.146.26.249` 覆盖 `/var/www/fam-ui`(macOS `._*` 元数据已清)。 + +**端到端验证**: +- `smart-camera.zichuan.xyz/timeline` → 200(SPA) +- `/login` → 302 → `auth.zichuan.xyz/authorize?...&redirect_uri=https://smart-camera.zichuan.xyz/api/auth/callback` +- `/api/auth/check`(无 cookie)→ `{"authed":false}`(白名单,不 401) +- `/api/ui/videos`(无 cookie)→ 401 → 前端据此自动跳 `/login` + +登录链路已通:未登录访问任意页面 → 首个 401 → 自动跳 auth-hub 登录 → 回调种 `fam_session` cookie → 回 `/timeline` 正常加载。 diff --git a/fam-ui/src/App.vue b/fam-ui/src/App.vue index 572b9b2..1ff251c 100644 --- a/fam-ui/src/App.vue +++ b/fam-ui/src/App.vue @@ -6,6 +6,16 @@ import { navItems } from './router.js' const route = useRoute() const sync = ref(null) +const authed = ref(false) + +async function checkAuth() { + try { + const r = await api.authCheck() + authed.value = !!r.authed + } catch { + authed.value = false + } +} async function refreshStatus() { try { @@ -16,8 +26,19 @@ async function refreshStatus() { } } +async function doLogout() { + try { + await fetch('/api/logout', { method: 'POST' }) + } catch { + // 忽略网络错误,下面强制跳转即可 + } + // 清掉 cookie 后回根路径,未登录态会让后端 401 -> 自动跳 /login 重新登录 + window.location.href = '/' +} + let timer = null onMounted(() => { + checkAuth() refreshStatus() timer = setInterval(refreshStatus, 30000) }) @@ -40,14 +61,34 @@ onUnmounted(() => clearInterval(timer)) 游标 {{ sync.cursor ? fmtDateTime(sync.cursor) : '(全量)' }}
⚠ {{ sync.last_error }}
+
+ + 🔑 登录 + + +
- +
🏠 家庭智能监控
- - {{ sync.running ? '同步中' : '未运行' }} - +
+ + 🔑 登录 + + + + {{ sync.running ? '同步中' : '未运行' }} + +
diff --git a/fam-ui/src/api.js b/fam-ui/src/api.js index d137eef..9edc7ac 100644 --- a/fam-ui/src/api.js +++ b/fam-ui/src/api.js @@ -1,5 +1,9 @@ // API 薄封装:生产环境同源相对路径;开发环境走 vite.config.js 的 /api 代理。 +// 全局:未登录时首个 401 直接跳转到 /login 走 OIDC/PKCE 登录流程。 +// 用一个模块级开关保证整次会话只触发一次跳转,避免多个并发 401 重复导航。 +let _redirectingToLogin = false + async function request(path, options = {}) { const res = await fetch(path, { headers: { 'Content-Type': 'application/json' }, @@ -12,6 +16,14 @@ async function request(path, options = {}) { // 非 JSON 响应(如 404 空 body),保持 data=null } if (!res.ok) { + // 401 未登录:交给后端 /login(302 到 auth-hub)发起统一登录。 + // /api/auth/check 等白名单接口不会返回 401,所以这里只会命中真正的鉴权失败。 + if (res.status === 401 && !_redirectingToLogin) { + _redirectingToLogin = true + window.location.href = '/login' + // 返回永挂起的 promise,阻止调用方继续渲染"未登录"错误态(页面即将跳转) + return new Promise(() => {}) + } const msg = (data && (data.error || data.message)) || `HTTP ${res.status}` throw new Error(msg) } @@ -52,6 +64,7 @@ export const api = { request('/api/member/merge', { method: 'POST', body: JSON.stringify({ source, target }) }), status: () => request('/api/status'), + authCheck: () => request('/api/auth/check'), syncTrigger: () => request('/api/sync/trigger', { method: 'POST' }), identityCorrect: (video_id, current_name, new_name) =>