mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 15:40:18 +00:00
fix: move Users to Settings subtab, bootstrap admin user on first run
- Moved Users from top-level tab to Settings sidebar subtab (under Skills, before Theme toggle) - On first startup with empty users table, automatically creates an admin user from GATEWAY_USER_ID config with a corresponding API token from GATEWAY_AUTH_TOKEN. This ensures the owner appears in the Users panel immediately. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,6 @@
|
||||
<button data-tab="memory" data-i18n="tab.memory">Memory</button>
|
||||
<button data-tab="jobs" data-i18n="tab.jobs">Jobs</button>
|
||||
<button data-tab="routines" data-i18n="tab.routines">Routines</button>
|
||||
<button data-tab="users" data-i18n="tab.users">Users</button>
|
||||
<button data-tab="settings" data-i18n="tab.settings">Settings</button>
|
||||
<div class="spacer"></div>
|
||||
|
||||
@@ -283,31 +282,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users Tab (admin) -->
|
||||
<div class="tab-panel" id="tab-users">
|
||||
<div class="users-container">
|
||||
<div class="users-header">
|
||||
<h3>User Management</h3>
|
||||
<button id="users-create-btn" class="btn-primary">+ New User</button>
|
||||
</div>
|
||||
<div id="users-create-form" style="display:none" class="users-form">
|
||||
<input type="text" id="user-display-name" placeholder="Display name" />
|
||||
<input type="email" id="user-email" placeholder="Email (optional)" />
|
||||
<select id="user-role"><option value="member">Member</option><option value="admin">Admin</option></select>
|
||||
<button id="users-create-submit" class="btn-primary">Create</button>
|
||||
<button id="users-create-cancel" class="btn-secondary">Cancel</button>
|
||||
</div>
|
||||
<div id="users-token-result" style="display:none" class="users-token-banner"></div>
|
||||
<table class="routines-table" id="users-table">
|
||||
<thead><tr>
|
||||
<th>ID</th><th>Display Name</th><th>Email</th><th>Role</th><th>Status</th><th>Created</th><th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody id="users-tbody"></tbody>
|
||||
</table>
|
||||
<div id="users-empty" class="empty-state" style="display:none">No users found. Create the first user to get started.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<div class="tab-panel" id="tab-settings">
|
||||
<div class="settings-layout">
|
||||
@@ -319,6 +293,7 @@
|
||||
<button class="settings-subtab" data-settings-subtab="extensions" data-i18n="tab.extensions">Extensions</button>
|
||||
<button class="settings-subtab" data-settings-subtab="mcp" data-i18n="settings.mcp">MCP</button>
|
||||
<button class="settings-subtab" data-settings-subtab="skills" data-i18n="tab.skills">Skills</button>
|
||||
<button class="settings-subtab" data-settings-subtab="users" data-i18n="settings.users">Users</button>
|
||||
<button class="settings-theme-toggle" id="settings-theme-toggle" data-i18n="theme.tooltipSystem" title="Toggle theme">Theme</button>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
@@ -416,6 +391,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-subpanel" id="settings-users">
|
||||
<div class="users-container">
|
||||
<div class="users-header">
|
||||
<h3>User Management</h3>
|
||||
<button id="users-create-btn" class="btn-primary">+ New User</button>
|
||||
</div>
|
||||
<div id="users-create-form" style="display:none" class="users-form">
|
||||
<input type="text" id="user-display-name" placeholder="Display name" />
|
||||
<input type="email" id="user-email" placeholder="Email (optional)" />
|
||||
<select id="user-role"><option value="member">Member</option><option value="admin">Admin</option></select>
|
||||
<button id="users-create-submit" class="btn-primary">Create</button>
|
||||
<button id="users-create-cancel" class="btn-secondary">Cancel</button>
|
||||
</div>
|
||||
<div id="users-token-result" style="display:none" class="users-token-banner"></div>
|
||||
<table class="routines-table" id="users-table">
|
||||
<thead><tr>
|
||||
<th>ID</th><th>Display Name</th><th>Email</th><th>Role</th><th>Status</th><th>Created</th><th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody id="users-tbody"></tbody>
|
||||
</table>
|
||||
<div id="users-empty" class="empty-state" style="display:none">No users found. Create the first user to get started.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+44
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user