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:
2026-03-24 23:48:48 -07:00
co-authored by Claude Opus 4.6
parent 18b8589549
commit 87886278bb
4 changed files with 52 additions and 10 deletions
+8 -1
View File
@@ -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()))?;
+18
View File
@@ -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,
})))
+23 -6
View File
@@ -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); });
});
+3 -3
View File
@@ -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>