@agenticmail/enterprise 0.5.296 → 0.5.297

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -295,7 +295,8 @@ function InlinePermissionPicker({ permissions, pageRegistry, onChange }) {
295
295
  // ─── Users Page ────────────────────────────────────
296
296
 
297
297
  export function UsersPage() {
298
- var { toast } = useApp();
298
+ var app = useApp();
299
+ var toast = app.toast;
299
300
  var [users, setUsers] = useState([]);
300
301
  var [creating, setCreating] = useState(false);
301
302
  var [form, setForm] = useState({ email: '', password: '', name: '', role: 'viewer', permissions: '*' });
@@ -343,22 +344,39 @@ export function UsersPage() {
343
344
  setResetting(false);
344
345
  };
345
346
 
346
- var deleteUser = async function(user) {
347
+ var toggleActive = async function(user) {
348
+ var action = user.isActive === false ? 'reactivate' : 'deactivate';
347
349
  var ok = await showConfirm({
348
- title: 'Delete User',
349
- message: 'Are you sure you want to delete "' + (user.name || user.email) + '"? This cannot be undone.',
350
- warning: 'The user will lose all access immediately.',
351
- danger: true,
352
- confirmText: 'Delete User'
350
+ title: action === 'deactivate' ? 'Deactivate User' : 'Reactivate User',
351
+ message: action === 'deactivate'
352
+ ? 'Deactivate "' + (user.name || user.email) + '"? They will be unable to log in and will see a message to contact their organization.'
353
+ : 'Reactivate "' + (user.name || user.email) + '"? They will be able to log in again.',
354
+ danger: action === 'deactivate',
355
+ confirmText: action === 'deactivate' ? 'Deactivate' : 'Reactivate'
353
356
  });
354
357
  if (!ok) return;
355
358
  try {
356
- await apiCall('/users/' + user.id, { method: 'DELETE' });
357
- toast('User deleted', 'success');
359
+ await apiCall('/users/' + user.id + '/' + action, { method: 'POST' });
360
+ toast('User ' + action + 'd', 'success');
358
361
  load();
359
362
  } catch (e) { toast(e.message, 'error'); }
360
363
  };
361
364
 
365
+ var [deleteStep, setDeleteStep] = useState(0);
366
+ var [deleteTarget, setDeleteTarget] = useState(null);
367
+ var [deleteTyped, setDeleteTyped] = useState('');
368
+
369
+ var startDelete = function(user) { setDeleteTarget(user); setDeleteStep(1); setDeleteTyped(''); };
370
+ var cancelDelete = function() { setDeleteTarget(null); setDeleteStep(0); setDeleteTyped(''); };
371
+
372
+ var confirmDelete = async function() {
373
+ try {
374
+ await apiCall('/users/' + deleteTarget.id, { method: 'DELETE', body: JSON.stringify({ confirmationToken: 'DELETE_USER_' + deleteTarget.email }) });
375
+ toast('User permanently deleted', 'success');
376
+ cancelDelete(); load();
377
+ } catch (e) { toast(e.message, 'error'); }
378
+ };
379
+
362
380
  var openPermissions = async function(user) {
363
381
  try {
364
382
  var d = await apiCall('/users/' + user.id + '/permissions');
@@ -485,18 +503,100 @@ export function UsersPage() {
485
503
  onClose: function() { setPermTarget(null); }
486
504
  }),
487
505
 
506
+ // 5-step delete confirmation modal
507
+ deleteTarget && h(Modal, {
508
+ title: 'Delete User — Step ' + deleteStep + ' of 5',
509
+ onClose: cancelDelete,
510
+ width: 480,
511
+ footer: h(Fragment, null,
512
+ h('button', { className: 'btn btn-secondary', onClick: deleteStep === 1 ? cancelDelete : function() { setDeleteStep(deleteStep - 1); } }, deleteStep === 1 ? 'Cancel' : 'Back'),
513
+ deleteStep < 5
514
+ ? h('button', { className: 'btn btn-' + (deleteStep >= 3 ? 'danger' : 'primary'), onClick: function() { setDeleteStep(deleteStep + 1); } }, 'Continue')
515
+ : h('button', { className: 'btn btn-danger', onClick: confirmDelete, disabled: deleteTyped !== deleteTarget.email }, 'Permanently Delete')
516
+ )
517
+ },
518
+ // Step 1: Warning
519
+ deleteStep === 1 && h('div', null,
520
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: 'var(--danger-soft, rgba(220,38,38,0.08))', borderRadius: 8, marginBottom: 16 } },
521
+ h('svg', { width: 24, height: 24, viewBox: '0 0 24 24', fill: 'none', stroke: 'var(--danger)', strokeWidth: 2 }, h('path', { d: 'M12 9v4m0 4h.01M10.29 3.86l-8.6 14.86A2 2 0 0 0 3.4 21h17.2a2 2 0 0 0 1.71-2.98L13.71 3.86a2 2 0 0 0-3.42 0z' })),
522
+ h('div', null,
523
+ h('strong', null, 'Permanent Deletion'),
524
+ h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginTop: 2 } }, 'This action cannot be undone.')
525
+ )
526
+ ),
527
+ h('p', { style: { fontSize: 13 } }, 'You are about to permanently delete the user account for:'),
528
+ h('div', { style: { padding: 12, background: 'var(--bg-tertiary)', borderRadius: 8, marginTop: 8 } },
529
+ h('strong', null, deleteTarget.name || 'Unnamed'), h('br'),
530
+ h('span', { style: { fontSize: 12, color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' } }, deleteTarget.email)
531
+ ),
532
+ h('p', { style: { fontSize: 12, color: 'var(--text-muted)', marginTop: 12 } }, 'Consider deactivating instead — deactivated users can be reactivated later.')
533
+ ),
534
+ // Step 2: Data loss
535
+ deleteStep === 2 && h('div', null,
536
+ h('h4', { style: { marginBottom: 12 } }, 'Data That Will Be Lost'),
537
+ h('ul', { style: { paddingLeft: 20, fontSize: 13, lineHeight: 1.8 } },
538
+ h('li', null, 'All login sessions will be terminated immediately'),
539
+ h('li', null, 'Audit log entries will be orphaned (no user reference)'),
540
+ h('li', null, 'Any API keys created by this user will be revoked'),
541
+ h('li', null, 'Permission grants and role assignments will be removed'),
542
+ h('li', null, '2FA configuration and backup codes will be destroyed')
543
+ )
544
+ ),
545
+ // Step 3: Impact
546
+ deleteStep === 3 && h('div', null,
547
+ h('h4', { style: { marginBottom: 12 } }, 'Impact Assessment'),
548
+ h('div', { style: { padding: 12, background: 'var(--warning-soft, rgba(245,158,11,0.08))', borderRadius: 8, fontSize: 13, lineHeight: 1.6 } },
549
+ h('p', null, 'If this user manages or supervises any agents, those agents will lose their manager assignment.'),
550
+ h('p', { style: { marginTop: 8 } }, 'If this user created approval workflows, pending approvals may become orphaned.'),
551
+ h('p', { style: { marginTop: 8 } }, 'Any scheduled tasks or cron jobs created by this user will continue to run but cannot be modified.')
552
+ )
553
+ ),
554
+ // Step 4: Alternative
555
+ deleteStep === 4 && h('div', null,
556
+ h('h4', { style: { marginBottom: 12 } }, 'Are You Sure?'),
557
+ h('div', { style: { padding: 16, background: 'var(--success-soft, rgba(21,128,61,0.08))', borderRadius: 8, marginBottom: 16 } },
558
+ h('strong', null, 'Recommended alternative: Deactivate'),
559
+ h('p', { style: { fontSize: 12, color: 'var(--text-secondary)', marginTop: 4 } }, 'Deactivating blocks login while preserving all data. The user can be reactivated at any time. This is the safe option.')
560
+ ),
561
+ h('div', { style: { padding: 16, background: 'var(--danger-soft, rgba(220,38,38,0.08))', borderRadius: 8 } },
562
+ h('strong', null, 'Permanent deletion'),
563
+ h('p', { style: { fontSize: 12, color: 'var(--text-secondary)', marginTop: 4 } }, 'Removes the user and all associated data forever. There is no recovery.')
564
+ )
565
+ ),
566
+ // Step 5: Type email to confirm
567
+ deleteStep === 5 && h('div', null,
568
+ h('h4', { style: { marginBottom: 12, color: 'var(--danger)' } }, 'Final Confirmation'),
569
+ h('p', { style: { fontSize: 13, marginBottom: 12 } }, 'Type the user\'s email address to confirm permanent deletion:'),
570
+ h('div', { style: { padding: 8, background: 'var(--bg-tertiary)', borderRadius: 6, fontFamily: 'var(--font-mono)', fontSize: 13, textAlign: 'center', marginBottom: 12 } }, deleteTarget.email),
571
+ h('input', {
572
+ className: 'input', type: 'text', value: deleteTyped,
573
+ onChange: function(e) { setDeleteTyped(e.target.value); },
574
+ placeholder: 'Type email to confirm',
575
+ autoFocus: true,
576
+ style: { fontFamily: 'var(--font-mono)', fontSize: 13, borderColor: deleteTyped === deleteTarget.email ? 'var(--danger)' : 'var(--border)' }
577
+ }),
578
+ deleteTyped && deleteTyped !== deleteTarget.email && h('div', { style: { fontSize: 11, color: 'var(--danger)', marginTop: 4 } }, 'Email does not match')
579
+ )
580
+ ),
581
+
488
582
  // Users table
489
583
  h('div', { className: 'card' },
490
584
  h('div', { className: 'card-body-flush' },
491
585
  users.length === 0 ? h('div', { style: { padding: 24, textAlign: 'center', color: 'var(--text-muted)' } }, 'No users')
492
586
  : h('table', null,
493
- h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'Email'), h('th', null, 'Role'), h('th', null, 'Access'), h('th', null, '2FA'), h('th', null, 'Created'), h('th', { style: { width: 180 } }, 'Actions'))),
587
+ h('thead', null, h('tr', null, h('th', null, 'Name'), h('th', null, 'Email'), h('th', null, 'Role'), h('th', null, 'Status'), h('th', null, 'Access'), h('th', null, '2FA'), h('th', null, 'Created'), h('th', { style: { width: 200 } }, 'Actions'))),
494
588
  h('tbody', null, users.map(function(u) {
495
589
  var isRestricted = u.role === 'member' || u.role === 'viewer';
496
- return h('tr', { key: u.id },
590
+ var isDeactivated = u.isActive === false;
591
+ var isSelf = u.id === ((app || {}).user || {}).id;
592
+ return h('tr', { key: u.id, style: isDeactivated ? { opacity: 0.6 } : {} },
497
593
  h('td', null, h('strong', null, u.name || '-')),
498
594
  h('td', null, h('span', { style: { fontFamily: 'var(--font-mono)', fontSize: 12 } }, u.email)),
499
595
  h('td', null, h('span', { className: 'badge badge-' + (u.role === 'owner' ? 'warning' : u.role === 'admin' ? 'primary' : 'neutral') }, u.role)),
596
+ h('td', null, isDeactivated
597
+ ? h('span', { className: 'badge badge-danger', style: { fontSize: 10 } }, 'Deactivated')
598
+ : h('span', { className: 'badge badge-success', style: { fontSize: 10 } }, 'Active')
599
+ ),
500
600
  h('td', null, permBadge(u)),
501
601
  h('td', null, u.totpEnabled ? h('span', { className: 'badge badge-success' }, 'On') : h('span', { className: 'badge badge-neutral' }, 'Off')),
502
602
  h('td', { style: { fontSize: 12, color: 'var(--text-muted)' } }, u.createdAt ? new Date(u.createdAt).toLocaleDateString() : '-'),
@@ -509,7 +609,15 @@ export function UsersPage() {
509
609
  style: !isRestricted ? { opacity: 0.4 } : {}
510
610
  }, I.shield()),
511
611
  h('button', { className: 'btn btn-ghost btn-sm', title: 'Reset Password', onClick: function() { setResetTarget(u); setNewPassword(''); } }, I.lock()),
512
- h('button', { className: 'btn btn-ghost btn-sm', title: 'Delete User', onClick: function() { deleteUser(u); }, style: { color: 'var(--danger)' } }, I.trash())
612
+ // Deactivate / Reactivate
613
+ !isSelf && h('button', {
614
+ className: 'btn btn-ghost btn-sm',
615
+ title: isDeactivated ? 'Reactivate User' : 'Deactivate User',
616
+ onClick: function() { toggleActive(u); },
617
+ style: { color: isDeactivated ? 'var(--success, #15803d)' : 'var(--warning, #f59e0b)' }
618
+ }, isDeactivated ? I.check() : I.pause()),
619
+ // Delete (owner only)
620
+ !isSelf && h('button', { className: 'btn btn-ghost btn-sm', title: 'Delete User Permanently', onClick: function() { startDelete(u); }, style: { color: 'var(--danger)' } }, I.trash())
513
621
  )
514
622
  )
515
623
  );
package/src/db/adapter.ts CHANGED
@@ -67,6 +67,7 @@ export interface User {
67
67
  totpBackupCodes?: string; // JSON array of hashed backup codes
68
68
  permissions?: any; // '*' or { pageId: true | string[] }
69
69
  mustResetPassword?: boolean;
70
+ isActive?: boolean;
70
71
  createdAt: Date;
71
72
  updatedAt: Date;
72
73
  lastLoginAt?: Date;
@@ -191,6 +191,7 @@ export class PostgresAdapter extends DatabaseAdapter {
191
191
  ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_backup_codes TEXT;
192
192
  ALTER TABLE users ADD COLUMN IF NOT EXISTS permissions JSONB DEFAULT '"*"';
193
193
  ALTER TABLE users ADD COLUMN IF NOT EXISTS must_reset_password BOOLEAN DEFAULT FALSE;
194
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE;
194
195
  `).catch(() => {});
195
196
  await client.query('COMMIT');
196
197
  } catch (err) {
@@ -709,6 +710,7 @@ export class PostgresAdapter extends DatabaseAdapter {
709
710
  totpSecret: r.totp_secret, totpEnabled: !!r.totp_enabled, totpBackupCodes: r.totp_backup_codes,
710
711
  permissions: r.permissions != null ? (typeof r.permissions === 'string' ? (() => { try { return JSON.parse(r.permissions); } catch { return '*'; } })() : r.permissions) : '*',
711
712
  mustResetPassword: !!r.must_reset_password,
713
+ isActive: r.is_active !== false && r.is_active !== 0, // default true
712
714
  createdAt: new Date(r.created_at), updatedAt: new Date(r.updated_at),
713
715
  lastLoginAt: r.last_login_at ? new Date(r.last_login_at) : undefined,
714
716
  };
package/src/db/sqlite.ts CHANGED
@@ -62,6 +62,7 @@ export class SqliteAdapter extends DatabaseAdapter {
62
62
  // Add permissions column if missing
63
63
  try { this.db.exec(`ALTER TABLE users ADD COLUMN permissions TEXT DEFAULT '"*"'`); } catch { /* exists */ }
64
64
  try { this.db.exec(`ALTER TABLE users ADD COLUMN must_reset_password INTEGER DEFAULT 0`); } catch { /* exists */ }
65
+ try { this.db.exec(`ALTER TABLE users ADD COLUMN is_active INTEGER DEFAULT 1`); } catch { /* exists */ }
65
66
  });
66
67
  tx();
67
68
  }