mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
fix: user creation shows token, + Token works, no password save popup
Three UI/UX fixes: 1. Create user now generates an initial API token and shows it in a copy-able banner instead of triggering the browser's password save dialog. Uses autocomplete="off" and type="text" for email field. 2. "+ Token" button works: exposed createTokenForUser/suspendUser/ activateUser on window for inline onclick handlers in dynamically generated table rows. Token creation uses showTokenBanner helper. 3. Admin token creation: POST /api/tokens now accepts optional "user_id" field when the requesting user is admin, allowing token creation for other users from the Users panel. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
@@ -53,8 +53,15 @@ pub async fn tokens_create_handler(
|
||||
// First 8 chars of the hex token as a prefix for identification.
|
||||
let token_prefix = &plaintext_token[..8];
|
||||
|
||||
// Admin users can create tokens for other users via optional "user_id" field.
|
||||
let target_user = body
|
||||
.get("user_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|_| user.role == "admin")
|
||||
.unwrap_or(&user.user_id);
|
||||
|
||||
let record = store
|
||||
.create_api_token(&user.user_id, &name, &hash, token_prefix, expires_at)
|
||||
.create_api_token(target_user, &name, &hash, token_prefix, expires_at)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use rand::RngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::channels::web::auth::{AdminUser, AuthenticatedUser};
|
||||
@@ -67,12 +70,27 @@ pub async fn users_create_handler(
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// Generate a first API token so the new user can authenticate immediately.
|
||||
let mut token_bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut token_bytes);
|
||||
let plaintext_token = hex::encode(token_bytes);
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(token_bytes);
|
||||
let token_hash: [u8; 32] = hasher.finalize().into();
|
||||
let token_prefix = &plaintext_token[..8];
|
||||
|
||||
let _token_record = store
|
||||
.create_api_token(&user_id, "initial", &token_hash, token_prefix, None)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"id": user_record.id,
|
||||
"email": user_record.email,
|
||||
"display_name": user_record.display_name,
|
||||
"status": user_record.status,
|
||||
"role": user_record.role,
|
||||
"token": plaintext_token,
|
||||
"created_at": user_record.created_at.to_rfc3339(),
|
||||
"created_by": user_record.created_by,
|
||||
})))
|
||||
|
||||
@@ -4408,14 +4408,28 @@ function createTokenForUser(userId, displayName) {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: tokenName, user_id: userId }),
|
||||
}).then(function(data) {
|
||||
var banner = document.getElementById('users-token-result');
|
||||
banner.style.display = 'block';
|
||||
banner.innerHTML = '<strong>Token created!</strong> Copy this now — it won\'t be shown again:<br>'
|
||||
+ '<code class="token-display">' + escapeHtml(data.token) + '</code>'
|
||||
+ '<button class="btn-small" onclick="navigator.clipboard.writeText(\'' + escapeHtml(data.token) + '\');this.textContent=\'Copied!\'">Copy</button>';
|
||||
showTokenBanner(data.token);
|
||||
}).catch(function(e) { alert('Failed to create token: ' + e.message); });
|
||||
}
|
||||
|
||||
function showTokenBanner(tokenValue) {
|
||||
var banner = document.getElementById('users-token-result');
|
||||
if (!banner) return;
|
||||
banner.style.display = 'block';
|
||||
banner.innerHTML = '<strong>Token created!</strong> Copy this now — it won\'t be shown again:<br>'
|
||||
+ '<code class="token-display" id="token-copy-value">' + escapeHtml(tokenValue) + '</code>'
|
||||
+ '<button class="btn-small" id="token-copy-btn">Copy</button>';
|
||||
document.getElementById('token-copy-btn').addEventListener('click', function() {
|
||||
navigator.clipboard.writeText(tokenValue);
|
||||
this.textContent = 'Copied!';
|
||||
});
|
||||
}
|
||||
|
||||
// Make functions accessible for delegated event handlers
|
||||
window.suspendUser = suspendUser;
|
||||
window.activateUser = activateUser;
|
||||
window.createTokenForUser = createTokenForUser;
|
||||
|
||||
// Wire up Users tab create form
|
||||
document.getElementById('users-create-btn')?.addEventListener('click', function() {
|
||||
document.getElementById('users-create-form').style.display = 'flex';
|
||||
@@ -4441,10 +4455,13 @@ document.getElementById('users-create-submit')?.addEventListener('click', functi
|
||||
email: email || undefined,
|
||||
role: role,
|
||||
}),
|
||||
}).then(function() {
|
||||
}).then(function(data) {
|
||||
document.getElementById('users-create-form').style.display = 'none';
|
||||
document.getElementById('user-display-name').value = '';
|
||||
document.getElementById('user-email').value = '';
|
||||
if (data.token) {
|
||||
showTokenBanner(data.token);
|
||||
}
|
||||
loadUsers();
|
||||
}).catch(function(e) { alert('Failed to create user: ' + e.message); });
|
||||
});
|
||||
|
||||
@@ -397,9 +397,9 @@
|
||||
<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)" />
|
||||
<div id="users-create-form" style="display:none" class="users-form" autocomplete="off">
|
||||
<input type="text" id="user-display-name" placeholder="Display name" autocomplete="off" />
|
||||
<input type="text" id="user-email" placeholder="Email (optional)" autocomplete="off" />
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user