Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file modified build/TianShanOS.bin
Binary file not shown.
Binary file modified build/www.bin
Binary file not shown.
51 changes: 51 additions & 0 deletions components/ts_api/src/ts_api_auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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",
Expand Down
15 changes: 10 additions & 5 deletions components/ts_core/ts_service/src/ts_service.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
8 changes: 3 additions & 5 deletions components/ts_net/src/ts_http_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

/* ===== 发送文件内容 ===== */
Expand Down
7 changes: 7 additions & 0 deletions components/ts_security/include/ts_security.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
13 changes: 13 additions & 0 deletions components/ts_security/src/ts_auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 检查用户是否已修改初始密码
*/
Expand Down
21 changes: 21 additions & 0 deletions components/ts_webui/web/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions components/ts_webui/web/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 86 additions & 17 deletions components/ts_webui/web/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -11661,7 +11702,7 @@ async function resetAdminPasswordToDefault() {
}
}

function toggleAdminPasswordVisibility(inputId, button) {
function toggleAccountPasswordVisibility(inputId, button) {
const input = document.getElementById(inputId);
if (!input || !button) return;

Expand Down Expand Up @@ -11693,28 +11734,56 @@ async function loadSecurityPage() {
<h2>${typeof t === 'function' ? t('securityPage.accountSecurity') : '账号安全'}</h2>
<p class="account-security-desc">
<i class="ri-information-line"></i>
${typeof t === 'function' ? t('securityPage.adminPasswordManagementDesc') : 'root 可在此恢复 admin 账号访问。'}
${typeof t === 'function' ? t('securityPage.accountSecurityDesc') : 'root 可在此管理 root 与 admin 账号密码。'}
</p>
<div class="account-password-form">
<div class="form-group account-password-group">
<label>${typeof t === 'function' ? t('securityPage.newAdminPassword') : 'admin 新密码'}</label>
<div class="account-password-field">
<input class="account-password-input" type="password" id="admin-new-password" autocomplete="new-password" placeholder="${typeof t === 'function' ? t('securityPage.newAdminPasswordPlaceholder') : '输入 admin 新密码'}">
<button type="button" class="password-visibility-toggle" onclick="toggleAdminPasswordVisibility('admin-new-password', this)" title="${showPasswordLabel}" aria-label="${showPasswordLabel}"><i class="ri-eye-line"></i></button>
<div class="account-password-block">
<h3>${typeof t === 'function' ? t('securityPage.rootPasswordManagement') : 'root 密码管理'}</h3>
<p class="account-password-desc">${typeof t === 'function' ? t('securityPage.rootPasswordManagementDesc') : '设置 root 新密码不会影响当前已登录会话。'}</p>
<div class="account-password-form">
<div class="form-group account-password-group">
<label>${typeof t === 'function' ? t('securityPage.newRootPassword') : 'root 新密码'}</label>
<div class="account-password-field">
<input class="account-password-input" type="password" id="root-new-password" autocomplete="new-password" placeholder="${typeof t === 'function' ? t('securityPage.newRootPasswordPlaceholder') : '输入 root 新密码'}">
<button type="button" class="password-visibility-toggle" onclick="toggleAccountPasswordVisibility('root-new-password', this)" title="${showPasswordLabel}" aria-label="${showPasswordLabel}"><i class="ri-eye-line"></i></button>
</div>
</div>
</div>
<div class="form-group account-password-group">
<label>${typeof t === 'function' ? t('securityPage.confirmAdminPassword') : '确认新密码'}</label>
<div class="account-password-field">
<input class="account-password-input" type="password" id="admin-confirm-password" autocomplete="new-password" placeholder="${typeof t === 'function' ? t('securityPage.confirmAdminPasswordPlaceholder') : '再次输入新密码'}">
<button type="button" class="password-visibility-toggle" onclick="toggleAdminPasswordVisibility('admin-confirm-password', this)" title="${showPasswordLabel}" aria-label="${showPasswordLabel}"><i class="ri-eye-line"></i></button>
<div class="form-group account-password-group">
<label>${typeof t === 'function' ? t('securityPage.confirmRootPassword') : '确认新密码'}</label>
<div class="account-password-field">
<input class="account-password-input" type="password" id="root-confirm-password" autocomplete="new-password" placeholder="${typeof t === 'function' ? t('securityPage.confirmRootPasswordPlaceholder') : '再次输入新密码'}">
<button type="button" class="password-visibility-toggle" onclick="toggleAccountPasswordVisibility('root-confirm-password', this)" title="${showPasswordLabel}" aria-label="${showPasswordLabel}"><i class="ri-eye-line"></i></button>
</div>
</div>
<div class="button-group account-security-actions">
<button class="btn btn-sm btn-service-style" onclick="submitRootPasswordSet()"><i class="ri-key-line"></i> ${typeof t === 'function' ? t('securityPage.setRootPassword') : '设置 root 新密码'}</button>
</div>
</div>
<div class="button-group account-security-actions">
<button class="btn btn-sm btn-service-style" onclick="submitAdminPasswordSet()"><i class="ri-key-line"></i> ${typeof t === 'function' ? t('securityPage.setAdminPassword') : '设置 admin 新密码'}</button>
<div id="root-password-error" class="form-error hidden"></div>
</div>
<div class="account-password-block">
<h3>${typeof t === 'function' ? t('securityPage.adminPasswordManagement') : 'admin 密码管理'}</h3>
<p class="account-password-desc">${typeof t === 'function' ? t('securityPage.adminPasswordManagementDesc') : 'root 可在此恢复 admin 账号访问。'}</p>
<div class="account-password-form">
<div class="form-group account-password-group">
<label>${typeof t === 'function' ? t('securityPage.newAdminPassword') : 'admin 新密码'}</label>
<div class="account-password-field">
<input class="account-password-input" type="password" id="admin-new-password" autocomplete="new-password" placeholder="${typeof t === 'function' ? t('securityPage.newAdminPasswordPlaceholder') : '输入 admin 新密码'}">
<button type="button" class="password-visibility-toggle" onclick="toggleAccountPasswordVisibility('admin-new-password', this)" title="${showPasswordLabel}" aria-label="${showPasswordLabel}"><i class="ri-eye-line"></i></button>
</div>
</div>
<div class="form-group account-password-group">
<label>${typeof t === 'function' ? t('securityPage.confirmAdminPassword') : '确认新密码'}</label>
<div class="account-password-field">
<input class="account-password-input" type="password" id="admin-confirm-password" autocomplete="new-password" placeholder="${typeof t === 'function' ? t('securityPage.confirmAdminPasswordPlaceholder') : '再次输入新密码'}">
<button type="button" class="password-visibility-toggle" onclick="toggleAccountPasswordVisibility('admin-confirm-password', this)" title="${showPasswordLabel}" aria-label="${showPasswordLabel}"><i class="ri-eye-line"></i></button>
</div>
</div>
<div class="button-group account-security-actions">
<button class="btn btn-sm btn-service-style" onclick="submitAdminPasswordSet()"><i class="ri-key-line"></i> ${typeof t === 'function' ? t('securityPage.setAdminPassword') : '设置 admin 新密码'}</button>
</div>
</div>
<div id="admin-password-error" class="form-error hidden"></div>
</div>
<div id="admin-password-error" class="form-error hidden" style="margin-bottom:12px"></div>
<div class="account-danger-row">
<div>
<strong>${typeof t === 'function' ? t('securityPage.dangerZone') : '危险操作'}</strong>
Expand Down
12 changes: 12 additions & 0 deletions components/ts_webui/web/js/lang/en-US.js
Original file line number Diff line number Diff line change
Expand Up @@ -1995,6 +1995,18 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', {
generateKey: 'Generate Key',
// Account security
accountSecurity: 'Account Security',
accountSecurityDesc: 'Manage root and admin account passwords from this root session.',
rootPasswordManagement: 'Root password management',
rootPasswordManagementDesc: 'Setting a new root password does not affect currently active sessions.',
newRootPassword: 'New root password',
newRootPasswordPlaceholder: 'Enter new root password',
confirmRootPassword: 'Confirm new password',
confirmRootPasswordPlaceholder: 'Enter the new password again',
setRootPassword: 'Set root password',
rootPasswordMismatch: 'The two root passwords do not match',
rootPasswordLength: 'Root password must be 4-64 characters',
rootPasswordSetSuccess: 'Root password updated',
adminPasswordManagement: 'Admin password management',
adminPasswordManagementDesc: 'Root can restore admin account access here. Setting a new password will not affect the current root session.',
newAdminPassword: 'New admin password',
newAdminPasswordPlaceholder: 'Enter new admin password',
Expand Down
12 changes: 12 additions & 0 deletions components/ts_webui/web/js/lang/zh-CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -1978,6 +1978,18 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', {
generateKey: '生成密钥',
// 账号安全
accountSecurity: '账号安全',
accountSecurityDesc: 'root 可在此管理 root 与 admin 账号密码。',
rootPasswordManagement: 'root 密码管理',
rootPasswordManagementDesc: '设置 root 新密码不会影响当前已登录会话。',
newRootPassword: 'root 新密码',
newRootPasswordPlaceholder: '输入 root 新密码',
confirmRootPassword: '确认新密码',
confirmRootPasswordPlaceholder: '再次输入新密码',
setRootPassword: '设置 root 新密码',
rootPasswordMismatch: '两次输入的 root 新密码不一致',
rootPasswordLength: 'root 密码长度必须为 4-64 个字符',
rootPasswordSetSuccess: 'root 密码已更新',
adminPasswordManagement: 'admin 密码管理',
adminPasswordManagementDesc: 'root 可在此恢复 admin 账号访问。设置新密码不会影响 root 当前会话。',
newAdminPassword: 'admin 新密码',
newAdminPasswordPlaceholder: '输入 admin 新密码',
Expand Down
Loading