Merge remote-tracking branch 'origin/dev' into fix/101-sense-layout-shell

# Conflicts:
#	docs/02-architecture-and-code-map.md
This commit is contained in:
QiuSW
2026-08-27 15:23:19 +08:00
16 changed files with 205 additions and 23 deletions
+1 -1
View File
@@ -228,7 +228,7 @@ function Initialize-SenseRuntime {
' enableddb: false',
' jwt:',
" secret: $(ConvertTo-SenseYamlString $jwtSecret)",
' timeout: 3600',
' timeout: 2592000',
' database:',
' driver: postgres',
" source: $(ConvertTo-SenseYamlString $databaseURL)",
+13 -8
View File
@@ -8,16 +8,11 @@ import (
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
)
const defaultLoginValidity = 30 * 24 * time.Hour
// AuthInit jwt验证new
func AuthInit() (*jwt.GinJWTMiddleware, error) {
timeout := time.Hour
if config.ApplicationConfig.Mode == "dev" {
timeout = time.Duration(876010) * time.Hour
} else {
if config.JwtConfig.Timeout != 0 {
timeout = time.Duration(config.JwtConfig.Timeout) * time.Second
}
}
timeout := resolveJWTTimeout(config.ApplicationConfig.Mode, config.JwtConfig.Timeout)
return jwt.New(&jwt.GinJWTMiddleware{
Realm: "Sense",
Key: []byte(config.JwtConfig.Secret),
@@ -34,3 +29,13 @@ func AuthInit() (*jwt.GinJWTMiddleware, error) {
})
}
func resolveJWTTimeout(mode string, configuredSeconds int64) time.Duration {
if mode == "dev" {
return time.Duration(876010) * time.Hour
}
if configuredSeconds > 0 {
return time.Duration(configuredSeconds) * time.Second
}
return defaultLoginValidity
}
@@ -0,0 +1,57 @@
package middleware
import (
"testing"
"time"
"github.com/go-admin-team/go-admin-core/sdk/config"
)
func TestResolveJWTTimeoutDefaultsToThirtyDays(t *testing.T) {
for _, mode := range []string{"prod", "test", "demo"} {
if got := resolveJWTTimeout(mode, 0); got != 30*24*time.Hour {
t.Errorf("mode %s timeout=%s, want 720h", mode, got)
}
}
}
func TestResolveJWTTimeoutHonorsExplicitConfiguration(t *testing.T) {
if got := resolveJWTTimeout("prod", 3600); got != time.Hour {
t.Fatalf("timeout=%s, want 1h", got)
}
}
func TestResolveJWTTimeoutKeepsUpstreamDevelopmentBehavior(t *testing.T) {
if got := resolveJWTTimeout("dev", 1); got != 876010*time.Hour {
t.Fatalf("development timeout=%s", got)
}
}
func TestAuthInitSignsTokenExpiringAfterThirtyDays(t *testing.T) {
oldMode := config.ApplicationConfig.Mode
oldTimeout := config.JwtConfig.Timeout
oldSecret := config.JwtConfig.Secret
t.Cleanup(func() {
config.ApplicationConfig.Mode = oldMode
config.JwtConfig.Timeout = oldTimeout
config.JwtConfig.Secret = oldSecret
})
config.ApplicationConfig.Mode = "prod"
config.JwtConfig.Timeout = 0
config.JwtConfig.Secret = "task-104-test-secret-not-for-production"
auth, err := AuthInit()
if err != nil {
t.Fatal(err)
}
now := time.Date(2026, 8, 17, 10, 0, 0, 0, time.UTC)
auth.TimeFunc = func() time.Time { return now }
_, expiresAt, err := auth.TokenGenerator(map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
if got := expiresAt.Sub(now); got != 30*24*time.Hour {
t.Fatalf("signed token validity=%s, want 720h", got)
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ settings:
# JWT加密字符串
secret: ""
# 过期时间单位:秒
timeout: 3600
timeout: 2592000
database:
# 数据库名称
name: dbname
+1 -1
View File
@@ -16,7 +16,7 @@ settings:
frontpath: ../ui/src
jwt:
secret: ""
timeout: 3600
timeout: 2592000
logger:
# 日志存放路径
path: temp/logs
+1 -1
View File
@@ -34,7 +34,7 @@ settings:
# token 密钥,生产环境时及的修改
secret: ""
# token 过期时间 单位:秒
timeout: 3600
timeout: 2592000
database:
# Sense 仅支持 PostgreSQL;连接信息由仓库外配置提供。
driver: postgres
+1 -1
View File
@@ -25,7 +25,7 @@ settings:
# token 密钥,生产环境时及的修改
secret: ""
# token 过期时间 单位:秒
timeout: 3600
timeout: 2592000
database:
# 文件名为上游兼容名称;Sense 仍只支持 PostgreSQL。
driver: postgres
+1 -1
View File
@@ -25,7 +25,7 @@ settings:
# 必填。生产环境至少 32 个字符;不得提交真实值。
secret: ""
# token 过期时间 单位:秒
timeout: 3600
timeout: 2592000
database:
# 数据库类型 mysql, sqlite3, postgres, sqlserver
# sqlserver: sqlserver://用户名:密码@地址?database=数据库名
+1
View File
@@ -65,6 +65,7 @@ try {
Assert-Equal 18071 $state.Port 'non-empty process environment must override sense.env'
Assert-True (-not (Test-Path -LiteralPath $marker)) 'sense.env content must never execute'
$generatedSettings = Get-Content -LiteralPath $state.SettingsPath -Raw
Assert-True $generatedSettings.Contains('timeout: 2592000') 'generated runtime settings must keep login valid for 30 days'
Assert-True ((Get-SenseEnvironmentValue -Name 'SENSE_DATABASE_URL').Contains('p#&;=x')) 'special characters must survive env parsing'
Assert-True ($generatedSettings.Contains('password=p#\u0026;=x')) 'special characters must be safely JSON-escaped in YAML'
Assert-True (-not ((Get-SenseDatabaseInfo (Get-SenseEnvironmentValue -Name 'SENSE_DATABASE_URL')).Sanitized.Contains('password='))) 'PostgreSQL tool arguments must not contain password'
+2 -1
View File
@@ -1,13 +1,14 @@
import Cookies from 'js-cookie'
const TokenKey = 'Sense-Admin-Token'
const LoginValidityDays = 30
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
return Cookies.set(TokenKey, token, { expires: LoginValidityDays })
}
export function removeToken() {
+32
View File
@@ -0,0 +1,32 @@
import Cookies from 'js-cookie'
import { getToken, removeToken, setToken } from '@/utils/auth'
jest.mock('js-cookie', () => ({
get: jest.fn(),
set: jest.fn(),
remove: jest.fn()
}))
describe('Sense login token cookie', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('persists the token for 30 days', () => {
setToken('test-token')
expect(Cookies.set).toHaveBeenCalledWith(
'Sense-Admin-Token',
'test-token',
{ expires: 30 }
)
})
it('reads and removes the same product-specific cookie', () => {
getToken()
removeToken()
expect(Cookies.get).toHaveBeenCalledWith('Sense-Admin-Token')
expect(Cookies.remove).toHaveBeenCalledWith('Sense-Admin-Token')
})
})
+3 -3
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Architecture-and-Code-Map
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Architecture-and-Code-Map.-
wiki_revision: 8608db441b21964aba7ba78e2c1581b4f5f910aa
synchronized_at: 2026-08-16T15:22:21Z
wiki_revision: 8df12aa3e136b1faee9c2dffb58ca39b045bc49f
synchronized_at: 2026-08-17T02:54:44Z
<!-- gitea-wiki-mirror:end -->
# 架构与代码地图
@@ -89,7 +89,7 @@ Sense 业务菜单必须遵循冻结 GoAdmin 的路由树:顶层目录使用 `
工单 #64 在该基线上重建 Sense 独立身份能力:`Sense/server/app/admin/apis/identity_bootstrap.go` 提供受外部高熵令牌保护的一次性首位管理员初始化,数据库迁移固定建立 `admin`、`implementation_operator`、`site_admin`、`viewer` 四个角色及最小 Casbin 权限;前端继续复用 go-admin-ui 动态菜单、权限按钮、请求封装与 Layout。仓库仍不提供默认账号、默认密码或可用 JWT 密钥。
Sense JWT realm 固定为 `Sense`;浏览器令牌 Cookie 为 `Sense-Admin-Token`,后端仅接受标准 Authorization Bearer 或独立的 `sense_session` Cookie,不接受查询参数令牌,也不得与 Bell 共享 JWT 密钥、Cookie 或账户库。登录成功/失败、登出、密码变更和鉴权拒绝写入身份审计;审计内容必须剔除密码、令牌、Cookie、验证码和其他秘密。配置管理 CRUD、configKey、set-config、接口管理等非产品必要路由不注册,即使管理员直接调用也返回 404。登录外壳必需的匿名只读 `GET /api/v1/app-config` 是唯一例外:它复用 GoAdmin `SysConfig.Get2SysApp`,只投影标记为前端可见的配置,不提供写入或管理能力。
Sense JWT realm 固定为 `Sense`;浏览器令牌 Cookie 为 `Sense-Admin-Token`,后端仅接受标准 Authorization Bearer 或独立的 `sense_session` Cookie,不接受查询参数令牌,也不得与 Bell 共享 JWT 密钥、Cookie 或账户库。Sense 默认登录有效期为固定 30 天:后端 JWT 使用 2,592,000 秒,前端 Token Cookie 使用 30 天持久化期限;不做滑动续期、Refresh Token 或服务端单 Token 撤销,退出只删除本机 Cookie。部署新版本后已有 Token 不会自动延长,用户必须重新登录取得新有效期。登录成功/失败、登出、密码变更和鉴权拒绝写入身份审计;审计内容必须剔除密码、令牌、Cookie、验证码和其他秘密。配置管理 CRUD、configKey、set-config、接口管理等非产品必要路由不注册,即使管理员直接调用也返回 404。登录外壳必需的匿名只读 `GET /api/v1/app-config` 是唯一例外:它复用 GoAdmin `SysConfig.Get2SysApp`,只投影标记为前端可见的配置,不提供写入或管理能力。
工单 #65 新增设备台账入口:后端按 `models → dto → service → api → router` 分层位于 `Sense/server/app/sense/device/`,管理路由在 `Sense/server/app/admin/router/sense_device.go`,前端页面位于 `Sense/ui/src/views/sense/device/index.vue`。设备凭据由 `Sense/server/app/sense/credential/` 独立存储和 AES-256-GCM 加密,HTTP 只返回是否已配置,不提供凭据读取接口。
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Local-Development-and-Verification
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Local-Development-and-Verification.-
wiki_revision: ee4819a84dd40846bc8fe1865e08590e1cf9cb81
synchronized_at: 2026-08-15T09:58:37Z
wiki_revision: 84a086640dc3618e43f834c488080f1e0cb5b063
synchronized_at: 2026-08-17T02:54:49Z
<!-- gitea-wiki-mirror:end -->
# 本地开发与验证
@@ -148,6 +148,8 @@ Remove-Variable bootstrapToken, bootstrapBody
初始化成功后停止服务,从启动环境中执行 `Remove-Item Env:SENSE_BOOTSTRAP_TOKEN`,再按正常生产方式启动。已有任一用户时初始化接口会拒绝请求。Sense 在 production、test、dev 模式均只提交账号和密码,不显示、不请求也不校验验证码;`/api/v1/captcha` 暂时保留作上游兼容接口,但登录页和登录 API 不依赖它。登录成功、错误密码和未认证拒绝仍必须写入脱敏身份审计,密码继续执行 6–72 字节策略。
Sense 默认登录有效期为固定 30 天。仓库配置和 Windows 运行脚本生成的 `jwt.timeout` 均为 `2592000` 秒,前端 `Sense-Admin-Token` Cookie 使用 30 天持久化期限。更新该版本后必须重新登录,已有 Token 不会自动延长。该有效期不是滑动续期;退出登录会删除本机 Cookie,但当前无状态 JWT 架构不提供服务端单 Token 撤销。如需立即使全部已签发 Token 失效,应在受控维护窗口轮换仓库外 JWT secret,并明确通知所有用户重新登录。
身份回归至少覆盖:admin 可管理账户及查看审计;implementation_operator 只能查看实施所需日志和字典支撑数据;site_admin 可维护账户并读取角色、部门、岗位、字典,但不能修改角色或菜单;viewer 不能访问管理接口。还要验证配置/接口管理路由返回 404、短密码被拒绝、6 位全小写密码可用,以及登录/登出/改密/拒绝审计中不含密码、令牌、Cookie 或验证码。身份审计直接写入 PostgreSQL,不依赖通用操作日志数据库开关。
详细来源与安全约束见 `Sense/LICENSES/SOURCES.md` 和 `Sense/README.md`。
+3 -3
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Product-Requirements
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Product-Requirements.-
wiki_revision: 221fd392c943caab13ee097c19a3d19d4410b65a
synchronized_at: 2026-08-15T09:59:15Z
wiki_revision: e6c6e0d7f658a7c1040fe569026e7d0ec581701a
synchronized_at: 2026-08-17T02:54:53Z
<!-- gitea-wiki-mirror:end -->
# 产品需求
@@ -48,7 +48,7 @@ YoVision 首个可交付目标是在民办寄宿学校以默认 16 路高风险
### P0
- **SEN-001 独立登录与权限**:Sense 自有用户、角色、菜单、会话和审计;至少覆盖管理员、实施/运维、站点管理员和只读边界。首期采用账号密码直接登录,所有运行模式均不使用验证码;密码保持 6–72 字节且不强制字符复杂度,登录成功与失败必须记录不含秘密的身份审计。
- **SEN-001 独立登录与权限**:Sense 自有用户、角色、菜单、会话和审计;至少覆盖管理员、实施/运维、站点管理员和只读边界。首期采用账号密码直接登录,所有运行模式均不使用验证码;密码保持 6–72 字节且不强制字符复杂度,登录成功与失败必须记录不含秘密的身份审计。默认登录有效期为固定 30 天,后端 JWT 与浏览器持久 Cookie 必须一致;版本更新后已有 Token 不自动延长,不提供滑动续期或服务端单 Token 撤销。
- **SEN-002 设备台账**:以 Device 为根实体,通过 `modality` 和 `capabilities` 表达 video/radar/contact/button/wearable/other;首期只完整实现 video,未实现适配器显示 `adapter_not_ready`。
- **SEN-003 ONVIF/RTSP 接入**:支持发现或手工添加、Profiles、StreamUri、主/子码流、校时、认证失败、重新探测和凭据更新。
- **SEN-004 批量开通**:默认 16 路可导入、预校验、待激活、逐项成功/失败、仅重试失败项;部分成功不做整体回滚。
@@ -0,0 +1,80 @@
<!-- gitea-wiki-mirror:start -->
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Task-104-Sense-30天登录有效期
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-104-Sense-30%E5%A4%A9%E7%99%BB%E5%BD%95%E6%9C%89%E6%95%88%E6%9C%9F.-
wiki_revision: 2b4df23ec321de868840da61d87d8d6fce8537a7
synchronized_at: 2026-08-17T03:46:13Z
<!-- gitea-wiki-mirror:end -->
# 104 Sense-30天登录有效期
- 类型:需求
- 所属 Epic:#7
- 所属 MVP / 版本:#8
- 状态:已完成
- 日期:2026-08-17
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/104
- Wiki 页面:Task-104-Sense-30天登录有效期
- Wiki revision:见本地镜像头
## 背景与目标
Sense 原生产、demo、SQLite 和完整配置默认 JWT 有效期为 3600 秒,前端 Token 使用浏览器会话 Cookie。按用户确认,将默认登录有效期统一调整为固定 30 天,使浏览器重启后在 Token 未过期时仍可保持登录。
## 最终方案
- 保留 go-admin JWT 中间件和 go-admin-ui `js-cookie` 封装;非开发模式未显式配置时默认使用 30 天,合法显式配置仍优先,开发模式保持上游行为。
- 生产、demo、SQLite、full 与 Windows 运行时生成配置统一使用 2,592,000 秒。
- `Sense-Admin-Token` Cookie 使用 `expires: 30`;退出登录仍删除 Cookie。
- 不是滑动续期,不增加 Refresh Token、服务端会话表或 Token 黑名单。已有 Token 不自动延长,部署后需重新登录。
- Token 泄露窗口扩大到 30 天;紧急失效需要轮换 JWT secret,这会使全部现有登录失效。
## 修改文件
- `Sense/server/common/middleware/auth.go`、`auth_test.go`:实现并验证默认 30 天 JWT。
- `Sense/server/config/settings*.yml`、`READMEN.md`:统一配置值和示例。
- `Sense/scripts/runtime/sense-common.ps1`、`Sense/tests/package/run-tests.ps1`:统一并验证 Windows 运行时配置。
- `Sense/ui/src/utils/auth.js`、`Sense/ui/tests/unit/utils/auth.spec.js`:持久化 Cookie 30 天并验证设置、读取和删除。
- `docs/02-architecture-and-code-map.md`、`docs/04-local-development-and-verification.md`、`docs/09-product-requirements.md`:由 Wiki 同步的长期文档镜像。
## 验收结果
| 验收标准 | 结果 |
|---|---|
| 非开发模式签发 JWT 的到期时间为签发后 30 天 | 通过 |
| Token Cookie 使用 30 天持久化期限,退出仍删除 | 通过 |
| 浏览器关闭后重新打开仍保持登录 | 通过(用户验收) |
| 所有运行配置及 Windows 生成值为 2,592,000 秒 | 通过 |
| Go、Vue、Windows 包测试与 PostgreSQL 启动回归 | 通过 |
| ZIP 仅含模板配置,不包含现场秘密或数据 | 通过 |
| 已有 Token 不自动延长并记录重新登录要求 | 通过 |
## 测试
- `go test ./...`:通过。
- `go vet ./...`:通过。
- `go build ./...`:通过。
- `pnpm run lint`:0 error,32 条上游既有 warning。
- `pnpm exec vue-cli-service test:unit --runInBand`:18 suites、48 tests 通过。
- `Sense/tests/package/run-tests.bat -PackageRoot Sense/dist/sense-windows-amd64`:22 assertions 通过。
- Windows amd64 生产构建、资源审计、包审计:通过;4 条既有构建 warning。
- 独立 #104 包使用现场 PostgreSQL 完成迁移和启动,`127.0.0.1:18080` 监听成功,生成配置确认 `timeout: 2592000`。
- 包:`Sense/dist/sense-windows-amd64-104.zip`,56,457,546 bytes,SHA-256 `BCE3EDD3F5BD79C75A953CB7673D1C9867CE3BB89755D6A188C7CA1226D05CE3`,来源提交 `adc227f110bf0c456d1523eac151ed1d8360cc83`。
- 一次错误的 Vue 测试参数把 `--runInBand` 识别为模式而未找到测试,已改用正确命令并通过;一次临时运行数据恢复把日志目录复制成同名文件,修正验证环境后从失败点重试并通过,均非产品代码缺陷。
- **未验证部分**:无法用真实等待 30 天验证自然到期;以固定时钟 JWT 测试和 Cookie 参数单测覆盖。用户已于 2026-08-17 明确验收通过,浏览器关闭/重开行为由人工确认。#104 独立包不包含仍待验收的 #101;两项进入 `dev` 后才能生成最终组合包。
## 遗留问题
- 当前服务已恢复为用户原有的 #101 验证包,避免应用外壳回退;#104 独立包保留为具名 ZIP,未覆盖当前标准包。
- 当前架构没有服务端单 Token 撤销能力,这是确认范围内的安全限制。
## 验收确认
- 用户于 2026-08-17 明确回复“#104 验收通过”。
- PR #105 按仓库门禁合入 `dev`;`main` 不变。
## 相关提交
- `857ba45` feat: 将 Sense 登录有效期调整为30天 (#104)
- `a843121` docs: 记录 Sense 30天登录有效期 (#104)
- `adc227f` test: 验证 Sense 30天 JWT 到期时间 (#104)
+4
View File
@@ -151,6 +151,10 @@
{
"page": "Task-99-Sense登录页只读配置接口",
"path": "docs/task/99-Sense登录页只读配置接口.md"
},
{
"page": "Task-104-Sense-30天登录有效期",
"path": "docs/task/104-Sense-30天登录有效期.md"
}
]
}