// Add-User modal (#10). Sits next to the invite modal in the members view, but // instead of emailing an invite it creates a user account directly with an // admin-set password and assigns them to this workspace + role. For // admin-provisioning on instances with no outbound email (where invites never // deliver). Mirrors workspace-members-invite-modal.js's structure. // // workspace: { id, name } // opts.onSuccess: (result) => void - fires on 201 (server response body) // opts.mapError: (err) => string - translates server error to display text import { api } from '../api.js'; import { t } from '../i18n.js'; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Crockford-ish readable random password: avoids ambiguous chars (0/O, 1/l/I). function generatePassword(len = 16) { const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'; const arr = new Uint32Array(len); crypto.getRandomValues(arr); let out = ''; for (let i = 0; i < len; i++) out += alphabet[arr[i] % alphabet.length]; return out; } export function openAddUserModal(workspace, opts = {}) { const { onSuccess, mapError } = opts; const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.innerHTML = ` `; document.body.appendChild(overlay); const emailInput = overlay.querySelector('#addUserEmail'); const nameInput = overlay.querySelector('#addUserName'); const pwInput = overlay.querySelector('#addUserPassword'); const genBtn = overlay.querySelector('#addUserGenerate'); const roleSelect = overlay.querySelector('#addUserRole'); const mustChange = overlay.querySelector('#addUserMustChange'); const errorEl = overlay.querySelector('#addUserError'); const submitBtn = overlay.querySelector('#addUserSubmit'); emailInput.focus(); function close() { overlay.remove(); document.removeEventListener('keydown', onKey); } function onKey(e) { if (e.key === 'Escape') close(); } document.addEventListener('keydown', onKey); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); overlay.querySelectorAll('[data-add-close]').forEach(b => b.addEventListener('click', close)); genBtn.addEventListener('click', () => { pwInput.value = generatePassword(); pwInput.type = 'text'; }); async function submit() { errorEl.style.display = 'none'; const email = emailInput.value.trim().toLowerCase(); const name = nameInput.value.trim(); const password = pwInput.value; const role = roleSelect.value; if (!email || !EMAIL_RE.test(email)) { showError(t('members.error.invalid_email')); emailInput.focus(); return; } if (!password || password.length < 8) { showError(t('members.error.password_min_8')); pwInput.focus(); return; } submitBtn.disabled = true; submitBtn.textContent = t('members.modal.creating'); try { const result = await api.adminCreateUser({ email, name, password, role, workspaceId: workspace.id, mustChangePassword: mustChange.checked, }); close(); if (typeof onSuccess === 'function') { try { onSuccess(result); } catch (e) { console.error('add-user modal onSuccess threw:', e); } } } catch (err) { submitBtn.disabled = false; submitBtn.textContent = t('members.modal.create'); const msg = (typeof mapError === 'function') ? mapError(err) : (err?.message || t('members.error.mutation_generic', { error: '' })); showError(msg); } } function showError(msg) { errorEl.textContent = msg; errorEl.style.display = 'block'; } submitBtn.addEventListener('click', submit); } function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }