diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a17d90..0ef85c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -159,6 +159,9 @@ jobs: - name: Check if release already exists (main branch) if: github.ref == 'refs/heads/main' id: check + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} run: | TAG="${{ steps.release_tag.outputs.tag }}" if gh release view "$TAG" 2>/dev/null; then diff --git a/build/TianShanOS.bin b/build/TianShanOS.bin index 8061ffd..552f5da 100644 Binary files a/build/TianShanOS.bin and b/build/TianShanOS.bin differ diff --git a/build/www.bin b/build/www.bin index 5a86540..f37185b 100644 Binary files a/build/www.bin and b/build/www.bin differ diff --git a/components/ts_api/src/ts_api_auth.c b/components/ts_api/src/ts_api_auth.c index a021362..d9eae28 100644 --- a/components/ts_api/src/ts_api_auth.c +++ b/components/ts_api/src/ts_api_auth.c @@ -328,6 +328,50 @@ static esp_err_t api_auth_admin_set_password(const cJSON *params, ts_api_result_ return ESP_OK; } +/** + * @brief auth.root.set_password - Root sets root password + * + * Params: { "token": "...", "new_password": "..." } + * Returns: { "success": true, "username": "root", "password_changed": true } + */ +static esp_err_t api_auth_root_set_password(const cJSON *params, ts_api_result_t *result) +{ + const cJSON *token_item = cJSON_GetObjectItem(params, "token"); + const cJSON *new_pwd_item = cJSON_GetObjectItem(params, "new_password"); + + esp_err_t ret = validate_root_token(token_item, result); + if (ret != ESP_OK) return ret; + + if (!cJSON_IsString(new_pwd_item)) { + ts_api_result_error(result, TS_API_ERR_INVALID_ARG, + "Missing required parameter: new_password"); + return ESP_ERR_INVALID_ARG; + } + + const char *new_password = new_pwd_item->valuestring; + size_t len = strlen(new_password); + if (len < 4 || len > 64) { + ts_api_result_error(result, TS_API_ERR_INVALID_ARG, + "Password must be 4-64 characters"); + return ESP_ERR_INVALID_ARG; + } + + ret = ts_auth_set_root_password(new_password); + if (ret != ESP_OK) { + ts_api_result_error(result, TS_API_ERR_INTERNAL, "Failed to set root password"); + return ret; + } + + cJSON *data = cJSON_CreateObject(); + cJSON_AddBoolToObject(data, "success", true); + cJSON_AddStringToObject(data, "username", "root"); + cJSON_AddBoolToObject(data, "password_changed", true); + + ts_api_result_ok(result, data); + TS_LOGI(TAG, "Root set root password"); + return ESP_OK; +} + /** * @brief auth.admin.reset_password - Root resets admin password to default * @@ -397,6 +441,13 @@ static const ts_api_endpoint_t s_auth_endpoints[] = { .handler = api_auth_admin_set_password, .requires_auth = false, /* Handler validates root token */ }, + { + .name = "auth.root.set_password", + .description = "Root sets root password", + .category = TS_API_CAT_SECURITY, + .handler = api_auth_root_set_password, + .requires_auth = false, /* Handler validates root token */ + }, { .name = "auth.admin.reset_password", .description = "Root resets admin password to default", diff --git a/components/ts_core/ts_service/src/ts_service.c b/components/ts_core/ts_service/src/ts_service.c index 37a014f..c1f39aa 100644 --- a/components/ts_core/ts_service/src/ts_service.c +++ b/components/ts_core/ts_service/src/ts_service.c @@ -750,13 +750,18 @@ esp_err_t ts_service_get_stats(ts_service_stats_t *stats) void ts_service_dump(void) { + ts_service_stats_t stats; + if (ts_service_get_stats(&stats) != ESP_OK) { + memset(&stats, 0, sizeof(stats)); + } + ESP_LOGI(TAG, "=== Service Status ==="); ESP_LOGI(TAG, "Total: %lu, Running: %lu, Stopped: %lu, Error: %lu", - (unsigned long)s_svc_ctx.stats.total_services, - (unsigned long)s_svc_ctx.stats.running_services, - (unsigned long)s_svc_ctx.stats.stopped_services, - (unsigned long)s_svc_ctx.stats.error_services); - ESP_LOGI(TAG, "Startup time: %lu ms", (unsigned long)s_svc_ctx.stats.startup_time_ms); + (unsigned long)stats.total_services, + (unsigned long)stats.running_services, + (unsigned long)stats.stopped_services, + (unsigned long)stats.error_services); + ESP_LOGI(TAG, "Startup time: %lu ms", (unsigned long)stats.startup_time_ms); xSemaphoreTake(s_svc_ctx.mutex, portMAX_DELAY); diff --git a/components/ts_net/src/ts_http_server.c b/components/ts_net/src/ts_http_server.c index 864747b..a8f7726 100644 --- a/components/ts_net/src/ts_http_server.c +++ b/components/ts_net/src/ts_http_server.c @@ -292,16 +292,14 @@ esp_err_t ts_http_send_file(ts_http_request_t *req, const char *filepath) httpd_resp_set_hdr(req->req, "Vary", "Accept-Encoding"); } - /* Static filenames are not content-hashed, so JS/CSS must revalidate after - * a WebUI partition update. Fonts/images can still be cached briefly. + /* index.html 不缓存(确保 OTA 后拿到新版本),其他静态资源长期缓存。 + * JS/CSS/字体等通过 index.html 中的 ?v= 版本参数实现缓存失效。 */ const char *basename = strrchr(filepath, '/'); if (basename && strcmp(basename, "/index.html") == 0) { httpd_resp_set_hdr(req->req, "Cache-Control", "no-cache, no-store, must-revalidate"); - } else if (ext && (strcmp(ext, ".js") == 0 || strcmp(ext, ".css") == 0)) { - httpd_resp_set_hdr(req->req, "Cache-Control", "no-cache, no-store, must-revalidate"); } else if (ext) { - httpd_resp_set_hdr(req->req, "Cache-Control", "public, max-age=3600"); + httpd_resp_set_hdr(req->req, "Cache-Control", "public, max-age=604800, immutable"); } /* ===== 发送文件内容 ===== */ diff --git a/components/ts_security/include/ts_security.h b/components/ts_security/include/ts_security.h index 1d32f68..a2f839e 100644 --- a/components/ts_security/include/ts_security.h +++ b/components/ts_security/include/ts_security.h @@ -163,6 +163,13 @@ esp_err_t ts_auth_change_password(const char *username, const char *old_password */ esp_err_t ts_auth_set_admin_password(const char *new_password); +/** + * @brief Set root password from an authenticated root session + * @param new_password New root password (4-64 chars) + * @return ESP_OK on success + */ +esp_err_t ts_auth_set_root_password(const char *new_password); + /** * @brief Check if user has changed the default password */ diff --git a/components/ts_security/src/ts_auth.c b/components/ts_security/src/ts_auth.c index e0cd758..03d92c0 100644 --- a/components/ts_security/src/ts_auth.c +++ b/components/ts_security/src/ts_auth.c @@ -315,6 +315,19 @@ esp_err_t ts_auth_set_admin_password(const char *new_password) return ret; } +/** + * @brief Root 管理功能:设置 root 密码 + */ +esp_err_t ts_auth_set_root_password(const char *new_password) +{ + esp_err_t ret = write_user_password_credential("root", new_password, true); + if (ret == ESP_OK) { + TS_LOGI(TAG, "Password set for root by root"); + } + + return ret; +} + /** * @brief 检查用户是否已修改初始密码 */ diff --git a/components/ts_webui/web/css/style.css b/components/ts_webui/web/css/style.css index b4c2801..12c6fda 100644 --- a/components/ts_webui/web/css/style.css +++ b/components/ts_webui/web/css/style.css @@ -3729,6 +3729,27 @@ button.btn-gray:hover, margin: 0 0 15px; } +.account-password-block { + margin-bottom: 16px; +} + +.account-password-block + .account-password-block { + border-top: 1px solid var(--border); + padding-top: 12px; +} + +.account-password-block h3 { + font-size: 1rem; + font-weight: 600; + margin: 0 0 6px; +} + +.account-password-desc { + color: var(--text-muted); + font-size: 0.9rem; + margin: 0 0 12px; +} + .account-password-form { display: grid; grid-template-columns: minmax(240px, 320px) minmax(240px, 320px) minmax(16px, 1fr) auto; diff --git a/components/ts_webui/web/js/api.js b/components/ts_webui/web/js/api.js index 9dddac6..79dd483 100644 --- a/components/ts_webui/web/js/api.js +++ b/components/ts_webui/web/js/api.js @@ -233,6 +233,13 @@ class TianShanAPI { }, 'POST'); } + async setRootPassword(newPassword) { + return this.call('auth.root.set_password', { + token: this.token, + new_password: newPassword + }, 'POST'); + } + async resetAdminPassword() { return this.call('auth.admin.reset_password', { token: this.token diff --git a/components/ts_webui/web/js/app.js b/components/ts_webui/web/js/app.js index a20fac3..77e01be 100644 --- a/components/ts_webui/web/js/app.js +++ b/components/ts_webui/web/js/app.js @@ -11602,6 +11602,47 @@ function clearExecResult() { // 安全页面 // ========================================================================= +async function submitRootPasswordSet() { + const newPwdEl = document.getElementById('root-new-password'); + const confirmPwdEl = document.getElementById('root-confirm-password'); + const errorEl = document.getElementById('root-password-error'); + const newPwd = newPwdEl ? newPwdEl.value : ''; + const confirmPwd = confirmPwdEl ? confirmPwdEl.value : ''; + + if (!errorEl) return; + + errorEl.classList.add('hidden'); + errorEl.textContent = ''; + + if (newPwd !== confirmPwd) { + errorEl.textContent = typeof t === 'function' ? t('securityPage.rootPasswordMismatch') : '两次输入的 root 新密码不一致'; + errorEl.classList.remove('hidden'); + return; + } + + if (newPwd.length < 4 || newPwd.length > 64) { + errorEl.textContent = typeof t === 'function' ? t('securityPage.rootPasswordLength') : 'root 密码长度必须为 4-64 个字符'; + errorEl.classList.remove('hidden'); + return; + } + + try { + const result = await api.setRootPassword(newPwd); + if (result.success || result.code === 0 || result.code === 'OK') { + if (newPwdEl) newPwdEl.value = ''; + if (confirmPwdEl) confirmPwdEl.value = ''; + api.passwordChanged = true; + showToast(typeof t === 'function' ? t('securityPage.rootPasswordSetSuccess') : 'root 密码已更新', 'success'); + } else { + errorEl.textContent = result.message || result.error || (typeof t === 'function' ? t('toast.saveFailed') : '保存失败'); + errorEl.classList.remove('hidden'); + } + } catch (error) { + errorEl.textContent = error.message || (typeof t === 'function' ? t('login.networkError') : '网络错误'); + errorEl.classList.remove('hidden'); + } +} + async function submitAdminPasswordSet() { const newPwdEl = document.getElementById('admin-new-password'); const confirmPwdEl = document.getElementById('admin-confirm-password'); @@ -11661,7 +11702,7 @@ async function resetAdminPasswordToDefault() { } } -function toggleAdminPasswordVisibility(inputId, button) { +function toggleAccountPasswordVisibility(inputId, button) { const input = document.getElementById(inputId); if (!input || !button) return; @@ -11693,28 +11734,56 @@ async function loadSecurityPage() {
- ${typeof t === 'function' ? t('securityPage.adminPasswordManagementDesc') : 'root 可在此恢复 admin 账号访问。'} + ${typeof t === 'function' ? t('securityPage.accountSecurityDesc') : 'root 可在此管理 root 与 admin 账号密码。'}
-${typeof t === 'function' ? t('securityPage.rootPasswordManagementDesc') : '设置 root 新密码不会影响当前已登录会话。'}
+${typeof t === 'function' ? t('securityPage.adminPasswordManagementDesc') : 'root 可在此恢复 admin 账号访问。'}
+