diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index 7892d4e6..9683bcce 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -2213,7 +2213,6 @@ function switchTab(tab) { if (tab === 'memory') loadMemoryTree(); if (tab === 'jobs') loadJobs(); if (tab === 'routines') loadRoutines(); - if (tab === 'users') loadUsers(); if (tab === 'logs') applyLogFilters(); if (tab === 'settings') { loadSettingsSubtab(currentSettingsSubtab); @@ -5044,7 +5043,7 @@ document.addEventListener('keydown', (e) => { // Mod+1-5: switch tabs if (mod && e.key >= '1' && e.key <= '5') { e.preventDefault(); - const tabs = ['chat', 'memory', 'jobs', 'routines', 'users', 'settings']; + const tabs = ['chat', 'memory', 'jobs', 'routines', 'settings']; const idx = parseInt(e.key) - 1; if (tabs[idx]) switchTab(tabs[idx]); return; @@ -5139,6 +5138,7 @@ function loadSettingsSubtab(subtab) { else if (subtab === 'extensions') { loadExtensions(); startPairingPoll(); } else if (subtab === 'mcp') loadMcpServers(); else if (subtab === 'skills') loadSkills(); + else if (subtab === 'users') loadUsers(); if (subtab !== 'extensions' && subtab !== 'channels') stopPairingPoll(); } diff --git a/src/channels/web/static/index.html b/src/channels/web/static/index.html index 4192ccc9..3a1370cb 100644 --- a/src/channels/web/static/index.html +++ b/src/channels/web/static/index.html @@ -97,7 +97,6 @@ -
@@ -283,31 +282,6 @@ - -
-
-
-

User Management

- -
- - - - - - - -
IDDisplay NameEmailRoleStatusCreatedActions
- -
-
-
@@ -319,6 +293,7 @@ +
@@ -416,6 +391,29 @@
+
+
+
+

User Management

+ +
+ + + + + + + +
IDDisplay NameEmailRoleStatusCreatedActions
+ +
+
diff --git a/src/main.rs b/src/main.rs index 8dfbfd48..d5ddc8ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -631,6 +631,50 @@ async fn async_main() -> anyhow::Result<()> { if let Some(ref d) = components.db { gw = gw.with_store(Arc::clone(d)); gw = gw.with_db_auth(Arc::clone(d)); + + // Bootstrap: create the first admin user from single-user config + // so the owner appears in the Users admin panel immediately. + if let Ok(false) = d.has_any_users().await { + let now = chrono::Utc::now(); + let user = ironclaw::db::UserRecord { + id: gw_config.user_id.clone(), + email: None, + display_name: gw_config.user_id.clone(), + status: "active".to_string(), + role: "admin".to_string(), + created_at: now, + updated_at: now, + last_login_at: None, + created_by: None, + metadata: serde_json::json!({"source": "bootstrap"}), + }; + if let Err(e) = d.create_user(&user).await { + tracing::warn!("Failed to bootstrap admin user: {}", e); + } else { + // Also create an API token from the gateway auth token so + // DB-backed auth works for the bootstrapped user. + let auth_token = gw.auth_token(); + if !auth_token.is_empty() { + use ironclaw::channels::web::auth::hash_token; + let hash = hash_token(auth_token); + let prefix = if auth_token.len() >= 8 { + &auth_token[..8] + } else { + auth_token + }; + if let Err(e) = d + .create_api_token(&gw_config.user_id, "bootstrap", &hash, prefix, None) + .await + { + tracing::warn!("Failed to create bootstrap token: {}", e); + } + } + tracing::info!( + user_id = gw_config.user_id, + "Bootstrapped admin user from gateway config" + ); + } + } } if let Some(ref jm) = container_job_manager { gw = gw.with_job_manager(Arc::clone(jm));