From 8acd29cb665afdbebd70304ac3bbcc2b52534826 Mon Sep 17 00:00:00 2001 From: Alfred Date: Thu, 26 Feb 2026 23:00:31 +0800 Subject: [PATCH 1/4] fix(security): evict same-user sessions when slots full on login - Add destroy_sessions_by_client() to free slots for re-login - When session slots are full, evict existing sessions for the same client_id before creating new session - Fixes 'Invalid credentials' on re-login after page refresh when slots were exhausted by orphaned sessions - Bump version to 0.4.4 --- README.md | 2 +- components/ts_security/src/ts_security.c | 31 ++++++++++++++++++++++-- version.txt | 2 +- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7251e54..de8a141 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ esptool.py --chip esp32s3 -p /dev/ttyACM0 write_flash \ ## 当前状态 -**版本**: 0.4.0 +**版本**: 0.4.4 **阶段**: Phase 38 完成 - WebUI 多语言支持 ### 已完成功能 diff --git a/components/ts_security/src/ts_security.c b/components/ts_security/src/ts_security.c index c97b10c..d3bdb02 100644 --- a/components/ts_security/src/ts_security.c +++ b/components/ts_security/src/ts_security.c @@ -166,6 +166,22 @@ esp_err_t ts_security_store_cert(const char *name, ts_cert_type_t type, return ret; } +/** + * @brief 驱逐指定 client_id 的所有会话(释放槽位) + * @note 仅内部使用,在 create_session 槽位满时按同用户驱逐 + */ +static void destroy_sessions_by_client(const char *client_id) +{ + if (!client_id || client_id[0] == '\0') return; + for (int i = 0; i < MAX_SESSIONS; i++) { + if (s_sessions[i].active && + strcmp(s_sessions[i].session.client_id, client_id) == 0) { + s_sessions[i].active = false; + TS_LOGI(TAG, "Evicted session for client: %s", client_id); + } + } +} + esp_err_t ts_security_create_session(const char *client_id, ts_perm_level_t level, uint32_t *session_id) { @@ -181,8 +197,19 @@ esp_err_t ts_security_create_session(const char *client_id, ts_perm_level_t leve } if (slot < 0) { - TS_LOGW(TAG, "No free session slots"); - return ESP_ERR_NO_MEM; + if (client_id && client_id[0] != '\0') { + destroy_sessions_by_client(client_id); + for (int i = 0; i < MAX_SESSIONS; i++) { + if (!s_sessions[i].active) { + slot = i; + break; + } + } + } + if (slot < 0) { + TS_LOGW(TAG, "No free session slots"); + return ESP_ERR_NO_MEM; + } } // Generate session ID diff --git a/version.txt b/version.txt index 17b2ccd..6f2743d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.4.3 +0.4.4 From f3a7023eb0e73b66702f112fa8e78026c6a5d3c8 Mon Sep 17 00:00:00 2001 From: Alfred Date: Thu, 26 Feb 2026 23:20:09 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(webui):=20=E4=BF=AE=E5=A4=8D=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E5=90=8E=E7=99=BB=E5=BD=95=E7=8A=B6=E6=80=81=E4=B8=A2?= =?UTF-8?q?=E5=A4=B1=E4=B8=8E=20i18n=20=E8=A6=86=E7=9B=96=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ts_webui_api: 登录响应增加 expires_in,修复 ts_expires=NaN 导致 isLoggedIn 失效 - index.html: translateDOM 后调用 updateAuthUI,避免语言包异步加载覆盖已登录用户名 - build.yml: main 分支编译成功后自动创建 Release,tag 已存在时跳过 --- .github/workflows/build.yml | 25 +++++++++++++++++++++---- components/ts_webui/src/ts_webui_api.c | 5 +++++ components/ts_webui/web/index.html | 2 ++ components/ts_webui/web/js/api.js | 9 +++++++-- components/ts_webui/web/js/app.js | 3 +-- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 560eb6d..1de6e2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,8 @@ jobs: build: name: Build ESP32-S3 Firmware runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} steps: - name: Checkout repository @@ -87,9 +89,9 @@ jobs: run: | if [ -f build/project_description.json ]; then VERSION=$(grep -o '"project_version":[^,]*' build/project_description.json | cut -d'"' -f4) - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "📦 Build version: $VERSION" fi + echo "version=${VERSION:-0.0.0}" >> $GITHUB_OUTPUT + echo "📦 Build version: ${VERSION:-0.0.0}" - name: Print build size run: | @@ -118,11 +120,11 @@ jobs: build/config/sdkconfig.h retention-days: 7 - # 可选:发布版本时创建 Release + # 编译成功后自动创建 Release:tag 推送 或 main 分支推送 release: name: Create Release needs: build - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main' && github.event_name == 'push') runs-on: ubuntu-latest permissions: contents: write # 创建 Release 需要写权限 @@ -134,9 +136,24 @@ jobs: pattern: tianshanos-firmware-* path: firmware + - name: Check if release already exists (main branch) + if: github.ref == 'refs/heads/main' + id: check + run: | + TAG="v${{ needs.build.outputs.version }}" + if gh release view "$TAG" 2>/dev/null; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "⏭️ Release $TAG already exists, skipping" + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + - name: Create Release + if: startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && steps.check.outputs.skip != 'true') uses: softprops/action-gh-release@v2 with: + tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', needs.build.outputs.version) }} + name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', needs.build.outputs.version) }} files: firmware/**/*.bin generate_release_notes: true env: diff --git a/components/ts_webui/src/ts_webui_api.c b/components/ts_webui/src/ts_webui_api.c index ee7e49d..84f94b8 100644 --- a/components/ts_webui/src/ts_webui_api.c +++ b/components/ts_webui/src/ts_webui_api.c @@ -305,6 +305,11 @@ static esp_err_t login_handler(ts_http_request_t *req, void *user_data) cJSON_AddNumberToObject(data, "session_id", session_id); cJSON_AddStringToObject(data, "username", username_copy); cJSON_AddStringToObject(data, "level", level_str); +#ifdef CONFIG_TS_SECURITY_TOKEN_EXPIRE_SEC + cJSON_AddNumberToObject(data, "expires_in", CONFIG_TS_SECURITY_TOKEN_EXPIRE_SEC); +#else + cJSON_AddNumberToObject(data, "expires_in", 86400); /* 24 hours */ +#endif cJSON_AddBoolToObject(data, "password_changed", password_changed); cJSON_AddItemToObject(response, "data", data); diff --git a/components/ts_webui/web/index.html b/components/ts_webui/web/index.html index 21e5ee1..f084e20 100644 --- a/components/ts_webui/web/index.html +++ b/components/ts_webui/web/index.html @@ -370,6 +370,8 @@

电压保护设置

var nameEl = document.getElementById('lang-name'); if (nameEl) nameEl.textContent = cur === 'zh-CN' ? '中文' : 'EN'; i18n.translateDOM(); + /* translateDOM 会覆盖 #user-name 为「未登录」,需在之后恢复已登录状态 */ + if (typeof updateAuthUI === 'function') setTimeout(updateAuthUI, 0); }; document.head.appendChild(s); })(); diff --git a/components/ts_webui/web/js/api.js b/components/ts_webui/web/js/api.js index 0384a89..fa06ae8 100644 --- a/components/ts_webui/web/js/api.js +++ b/components/ts_webui/web/js/api.js @@ -222,7 +222,10 @@ class TianShanAPI { // 尝试从 localStorage 恢复 const savedToken = localStorage.getItem('ts_token'); const expires = localStorage.getItem('ts_expires'); - if (savedToken && expires && Date.now() < parseInt(expires)) { + const parsed = expires ? parseInt(expires) : NaN; + const now = Date.now(); + const valid = savedToken && expires && now < parsed; + if (valid) { this.token = savedToken; this.username = localStorage.getItem('ts_username'); this.level = localStorage.getItem('ts_level'); @@ -232,7 +235,9 @@ class TianShanAPI { } // 检查是否过期 const expires = localStorage.getItem('ts_expires'); - if (expires && Date.now() >= parseInt(expires)) { + const parsed = expires ? parseInt(expires) : NaN; + const expired = expires && Date.now() >= parsed; + if (expired) { this.logout(); // 清理过期的 token return false; } diff --git a/components/ts_webui/web/js/app.js b/components/ts_webui/web/js/app.js index 26e63ed..e3093da 100644 --- a/components/ts_webui/web/js/app.js +++ b/components/ts_webui/web/js/app.js @@ -194,7 +194,6 @@ document.addEventListener('DOMContentLoaded', () => { function updateAuthUI() { const loginBtn = document.getElementById('login-btn'); const userName = document.getElementById('user-name'); - if (api.isLoggedIn()) { const username = api.getUsername(); const level = api.getLevel(); @@ -7673,7 +7672,7 @@ async function loadFilesPage() {