···
background: rgba(0, 0, 0, 0.04);
-
background: rgba(0, 0, 0, 0.5);
-
justify-content: center;
-
background: var(--background);
-
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.3);
-
border-bottom: 2px solid var(--secondary);
-
justify-content: space-between;
-
background: transparent;
-
justify-content: center;
-
transition: background 0.2s;
-
background: var(--secondary);
-
.detail-section:last-child {
-
.detail-section-title {
-
padding-bottom: 0.5rem;
-
border-bottom: 2px solid var(--secondary);
-
justify-content: space-between;
-
border-bottom: 1px solid var(--secondary);
-
.detail-row:last-child {
-
padding: 0.5rem 0.75rem;
-
border: 2px solid var(--secondary);
-
background: var(--background);
-
border-color: var(--primary);
-
background: var(--primary);
-
background: var(--gunmetal);
-
.btn-primary:disabled {
-
justify-content: space-between;
-
border: 2px solid var(--secondary);
-
.passkey-item:last-child {
-
margin-bottom: 0.25rem;
-
padding: 0.25rem 0.75rem;
-
background: rgba(0, 0, 0, 0.02);
-
padding: 0.25rem 0.75rem;
-
.password-status.has-password {
-
.password-status.no-password {
···
-
justify-content: space-between;
-
border: 2px solid var(--secondary);
-
.session-item:last-child {
-
margin-bottom: 0.25rem;
-
background: rgba(0, 0, 0, 0.02);
-
justify-content: space-between;
···
-
<div id="user-modal" class="modal">
-
<div class="modal-content">
-
<div class="modal-header">
-
<h2 class="modal-title">User Details</h2>
-
<button class="modal-close" aria-label="Close">×</button>
-
<div class="modal-body">
-
<div class="detail-section">
-
<h3 class="detail-section-title">User Information</h3>
-
<div class="detail-row">
-
<span class="detail-label">Email</span>
-
<span class="detail-value" id="modal-email">-</span>
-
<div class="detail-row">
-
<span class="detail-label">Name</span>
-
<span class="detail-value" id="modal-name">-</span>
-
<div class="detail-row">
-
<span class="detail-label">Role</span>
-
<span class="detail-value" id="modal-role">-</span>
-
<div class="detail-row">
-
<span class="detail-label">Joined</span>
-
<span class="detail-value" id="modal-joined">-</span>
-
<div class="detail-row">
-
<span class="detail-label">Last Login</span>
-
<span class="detail-value" id="modal-last-login">-</span>
-
<div class="detail-row">
-
<span class="detail-label">Transcriptions</span>
-
<span class="detail-value" id="modal-transcription-count">-</span>
-
<div class="detail-row">
-
<span class="detail-label">Password Status</span>
-
<span id="modal-password-status">-</span>
-
<div class="detail-section">
-
<h3 class="detail-section-title">Change Name</h3>
-
<form id="change-name-form">
-
<div class="form-group">
-
<label class="form-label" for="new-name">New Name</label>
-
<input type="text" id="new-name" class="form-input" placeholder="Enter new name">
-
<button type="submit" class="btn btn-primary">Update Name</button>
-
<div class="detail-section">
-
<h3 class="detail-section-title">Change Email</h3>
-
<form id="change-email-form">
-
<div class="form-group">
-
<label class="form-label" for="new-email">New Email</label>
-
<input type="email" id="new-email" class="form-input" placeholder="Enter new email">
-
<button type="submit" class="btn btn-primary">Update Email</button>
-
<div class="detail-section">
-
<h3 class="detail-section-title">Change Password</h3>
-
<form id="change-password-form">
-
<div class="form-group">
-
<label class="form-label" for="new-password">New Password</label>
-
<input type="password" id="new-password" class="form-input" placeholder="Enter new password (min 8 characters)">
-
<button type="submit" class="btn btn-primary">Update Password</button>
-
<div class="detail-section">
-
<h3 class="detail-section-title">Active Sessions</h3>
-
<div class="section-actions">
-
<span class="detail-label" id="session-count">0 active sessions</span>
-
<button id="logout-all-btn" class="btn btn-danger btn-small">Logout All Devices</button>
-
<div id="sessions-container">
-
<div class="loading">Loading sessions...</div>
-
<div class="detail-section">
-
<h3 class="detail-section-title">Passkeys</h3>
-
<div id="passkeys-container">
-
<div class="loading">Loading passkeys...</div>
<script type="module" src="../components/auth.ts"></script>
const errorMessage = document.getElementById('error-message');
const loading = document.getElementById('loading');
···
const transcriptionsTable = document.getElementById('transcriptions-table');
const usersTable = document.getElementById('users-table');
const userModal = document.getElementById('user-modal');
-
const modalClose = userModal.querySelector('.modal-close');
let currentUserEmail = null;
-
let currentModalUserId = null;
let userSortKey = 'created_at';
let userSortDirection = 'desc';
···
return date.toLocaleString();
-
function parseUserAgent(userAgent) {
-
if (!userAgent) return '🖥️ Unknown Device';
-
if (userAgent.includes('iPhone')) return '📱 iPhone';
-
if (userAgent.includes('iPad')) return '📱 iPad';
-
if (userAgent.includes('Android')) return '📱 Android';
-
if (userAgent.includes('Mac')) return '💻 Mac';
-
if (userAgent.includes('Windows')) return '💻 Windows';
-
if (userAgent.includes('Linux')) return '💻 Linux';
-
return '🖥️ Unknown Device';
function openUserModal(userId) {
-
currentModalUserId = userId;
-
userModal.classList.add('active');
-
loadUserDetails(userId);
function closeUserModal() {
-
userModal.classList.remove('active');
-
currentModalUserId = null;
-
async function loadUserDetails(userId) {
-
const res = await fetch(`/api/admin/users/${userId}/details`);
-
throw new Error('Failed to load user details');
-
const user = await res.json();
-
document.getElementById('modal-email').textContent = user.email;
-
document.getElementById('modal-name').textContent = user.name || 'Not set';
-
document.getElementById('modal-role').textContent = user.role;
-
document.getElementById('modal-joined').textContent = formatTimestamp(user.created_at);
-
document.getElementById('modal-last-login').textContent = user.last_login ? formatTimestamp(user.last_login) : 'Never';
-
document.getElementById('modal-transcription-count').textContent = user.transcriptionCount;
-
const passwordStatus = document.getElementById('modal-password-status');
-
if (user.hasPassword) {
-
passwordStatus.innerHTML = '<span class="password-status has-password">Has password</span>';
-
passwordStatus.innerHTML = '<span class="password-status no-password">No password (passkey only)</span>';
-
document.getElementById('new-name').value = user.name || '';
-
document.getElementById('new-email').value = user.email;
-
renderSessions(user.sessions, userId);
-
renderPasskeys(user.passkeys, userId);
-
alert('Failed to load user details');
-
function renderSessions(sessions, userId) {
-
const container = document.getElementById('sessions-container');
-
const sessionCount = document.getElementById('session-count');
-
const logoutAllBtn = document.getElementById('logout-all-btn');
-
sessionCount.textContent = `${sessions.length} active session${sessions.length !== 1 ? 's' : ''}`;
-
if (sessions.length === 0) {
-
container.innerHTML = '<div class="empty-sessions">No active sessions</div>';
-
logoutAllBtn.disabled = true;
-
logoutAllBtn.disabled = false;
-
const list = document.createElement('ul');
-
list.className = 'session-list';
-
list.innerHTML = sessions.map(s => `
-
<li class="session-item">
-
<div class="session-info">
-
<div class="session-device">${parseUserAgent(s.user_agent)}</div>
-
<div class="session-meta">
-
IP: ${s.ip_address || 'Unknown'} •
-
Created: ${formatTimestamp(s.created_at)} •
-
Expires: ${formatTimestamp(s.expires_at)}
-
<div class="session-actions">
-
<button class="btn btn-danger btn-small revoke-session-btn" data-session-id="${s.id}" data-user-id="${userId}">
-
container.innerHTML = '';
-
container.appendChild(list);
-
// Add revoke event listeners
-
list.querySelectorAll('.revoke-session-btn').forEach(btn => {
-
btn.addEventListener('click', async (e) => {
-
const button = e.target;
-
const sessionId = button.dataset.sessionId;
-
const userId = button.dataset.userId;
-
if (!confirm('Revoke this session? The user will be logged out of this device.')) {
-
button.disabled = true;
-
button.textContent = 'Revoking...';
-
const res = await fetch(`/api/admin/users/${userId}/sessions/${sessionId}`, {
-
throw new Error('Failed to revoke session');
-
await loadUserDetails(userId);
-
alert('Failed to revoke session');
-
button.disabled = false;
-
button.textContent = 'Revoke';
-
function renderPasskeys(passkeys, userId) {
-
const container = document.getElementById('passkeys-container');
-
if (passkeys.length === 0) {
-
container.innerHTML = '<div class="empty-passkeys">No passkeys registered</div>';
-
const list = document.createElement('ul');
-
list.className = 'passkey-list';
-
list.innerHTML = passkeys.map(pk => `
-
<li class="passkey-item">
-
<div class="passkey-info">
-
<div class="passkey-name">${pk.name || 'Unnamed Passkey'}</div>
-
<div class="passkey-meta">
-
Created: ${formatTimestamp(pk.created_at)}
-
${pk.last_used_at ? ` • Last used: ${formatTimestamp(pk.last_used_at)}` : ''}
-
<div class="passkey-actions">
-
<button class="btn btn-danger btn-small revoke-passkey-btn" data-passkey-id="${pk.id}" data-user-id="${userId}">
-
container.innerHTML = '';
-
container.appendChild(list);
-
list.querySelectorAll('.revoke-passkey-btn').forEach(btn => {
-
btn.addEventListener('click', async (e) => {
-
const button = e.target;
-
const passkeyId = button.dataset.passkeyId;
-
const userId = button.dataset.userId;
-
if (!confirm('Are you sure you want to revoke this passkey? The user will no longer be able to use it to sign in.')) {
-
button.disabled = true;
-
button.textContent = 'Revoking...';
-
const res = await fetch(`/api/admin/users/${userId}/passkeys/${passkeyId}`, {
-
throw new Error('Failed to revoke passkey');
-
await loadUserDetails(userId);
-
alert('Failed to revoke passkey');
-
button.disabled = false;
-
button.textContent = 'Revoke';
-
modalClose.addEventListener('click', closeUserModal);
userModal.addEventListener('click', (e) => {
if (e.target === userModal) {
-
document.getElementById('change-name-form').addEventListener('submit', async (e) => {
-
const name = document.getElementById('new-name').value.trim();
-
alert('Please enter a name');
-
const submitBtn = e.target.querySelector('button[type="submit"]');
-
submitBtn.disabled = true;
-
submitBtn.textContent = 'Updating...';
-
const res = await fetch(`/api/admin/users/${currentModalUserId}/name`, {
-
headers: {'Content-Type': 'application/json'},
-
body: JSON.stringify({name})
-
throw new Error('Failed to update name');
-
alert('Name updated successfully');
-
await loadUserDetails(currentModalUserId);
-
alert('Failed to update name');
-
submitBtn.disabled = false;
-
submitBtn.textContent = 'Update Name';
-
document.getElementById('change-email-form').addEventListener('submit', async (e) => {
-
const email = document.getElementById('new-email').value.trim();
-
if (!email || !email.includes('@')) {
-
alert('Please enter a valid email');
-
const submitBtn = e.target.querySelector('button[type="submit"]');
-
submitBtn.disabled = true;
-
submitBtn.textContent = 'Updating...';
-
const res = await fetch(`/api/admin/users/${currentModalUserId}/email`, {
-
headers: {'Content-Type': 'application/json'},
-
body: JSON.stringify({email})
-
const data = await res.json();
-
throw new Error(data.error || 'Failed to update email');
-
alert('Email updated successfully');
-
await loadUserDetails(currentModalUserId);
-
alert(error.message || 'Failed to update email');
-
submitBtn.disabled = false;
-
submitBtn.textContent = 'Update Email';
-
document.getElementById('change-password-form').addEventListener('submit', async (e) => {
-
const password = document.getElementById('new-password').value;
-
if (password.length < 8) {
-
alert('Password must be at least 8 characters');
-
if (!confirm('Are you sure you want to change this user\'s password? This will log them out of all devices.')) {
-
const submitBtn = e.target.querySelector('button[type="submit"]');
-
submitBtn.disabled = true;
-
submitBtn.textContent = 'Updating...';
-
const res = await fetch(`/api/admin/users/${currentModalUserId}/password`, {
-
headers: {'Content-Type': 'application/json'},
-
body: JSON.stringify({password})
-
throw new Error('Failed to update password');
-
alert('Password updated successfully. User has been logged out of all devices.');
-
document.getElementById('new-password').value = '';
-
await loadUserDetails(currentModalUserId);
-
alert('Failed to update password');
-
submitBtn.disabled = false;
-
submitBtn.textContent = 'Update Password';
-
document.getElementById('logout-all-btn').addEventListener('click', async (e) => {
-
if (!confirm('Are you sure you want to logout this user from ALL devices? This will terminate all active sessions.')) {
-
const button = e.target;
-
button.disabled = true;
-
button.textContent = 'Logging out...';
-
const res = await fetch(`/api/admin/users/${currentModalUserId}/sessions`, {
-
throw new Error('Failed to logout all devices');
-
alert('User logged out from all devices');
-
await loadUserDetails(currentModalUserId);
-
alert('Failed to logout all devices');
-
button.disabled = false;
-
button.textContent = 'Logout All Devices';