// VaultSentinel - Account Management Application // Main JavaScript functionality class AccountManager { constructor() { // Check if user is logged in const currentUser = this.loadFromStorage('currentUser'); if (!currentUser) { window.location.href = '../pages/login.html'; return; } this.currentUser = currentUser; this.accounts = []; this.applications = []; this.users = []; this.roles = []; this.accountPage = 1; this.accountPageSize = 9; this.appPage = 1; this.appPageSize = 9; this.userPage = 1; this.userPageSize = 9; this.apiBase = '/api'; this.currentPage = 'dashboard'; this.accountSearchTerm = ''; this.applicationSearchTerm = ''; this.accountServiceFilter = ''; this.userSearchTerm = ''; this.userRoleFilter = ''; this.mobileBreakpoint = 900; this.boundResizeHandler = null; this.configureNotifications(); this.initPromise = this.init(); this.pendingAccountAppId = undefined; } configureNotifications() { if (window.Notiflix?.Notify) { Notiflix.Notify.init({ position: 'right-top', timeout: 2500, clickToClose: true, pauseOnHover: true, distance: '12px', fontSize: '14px' }); } } notifySuccess(message) { if (window.Notiflix?.Notify) { Notiflix.Notify.success(message); } else { alert(message); } } notifyFailure(message) { if (window.Notiflix?.Notify) { Notiflix.Notify.failure(message); } else { alert(message); } } notifyWarning(message) { if (window.Notiflix?.Notify) { Notiflix.Notify.warning(message); } else { alert(message); } } getUserId() { const u = this.currentUser; const detected = u?.UserId ?? u?.userId ?? u?.id ?? u?.ID ?? u?.userid ?? u?.user_id ?? u?.user?.UserId ?? u?.user?.userId; // Fallback: if only username/role exist (no id), use default admin id = 1 return detected ?? 1; } getCurrentUserRoleRaw() { return this.currentUser?.Role ?? this.currentUser?.role ?? this.currentUser?.RoleName ?? this.currentUser?.user?.Role ?? this.currentUser?.user?.role ?? ''; } getCurrentUserRole() { return String(this.getCurrentUserRoleRaw() || '').trim().toLowerCase(); } isCurrentUserAdmin() { return this.getCurrentUserRole() === 'admin'; } getAuthHeaders(includeJson = false) { const headers = { 'x-user-id': String(this.getUserId()), 'x-user-role': this.getCurrentUserRole() }; if (includeJson) { headers['Content-Type'] = 'application/json'; } return headers; } async init() { await this.fetchApplications(); await this.fetchAccounts(); // Check if user is admin and fetch users/roles if (this.isCurrentUserAdmin()) { await this.fetchUsers(); await this.fetchRoles(); // Show Users menu const usersNav = document.getElementById('usersNav'); if (usersNav) usersNav.style.display = ''; } this.setupEventListeners(); this.setupResponsiveShell(); this.loadModals(); // Load modals từ file riêng // Single-page navigation based on hash this.handleRoute(location.hash || '#dashboard'); window.addEventListener('hashchange', () => this.handleRoute(location.hash)); } handleRoute(hash) { const route = (hash || '#dashboard').replace('#', '') || 'dashboard'; if (this.isMobileViewport()) { this.closeMobileNav(); } this.renderView(route); } renderView(page) { this.currentPage = page; const mainContent = document.getElementById('mainContent'); if (!mainContent) return; if (page === 'applications') { mainContent.innerHTML = this.getApplicationsContent(); this.setupAccountRowListeners(); this.setupAddButtonListeners(); this.setupFilters(); this.setupAppPagerListeners(); } else if (page === 'accounts') { mainContent.innerHTML = this.getAccountsContent(); this.setupAccountRowListeners(); this.setupAddButtonListeners(); this.setupFilters(); this.setupAccountPagerListeners(); } else if (page === 'users') { // Check if user is admin if (!this.isCurrentUserAdmin()) { mainContent.innerHTML = this.renderDashboard(); } else { mainContent.innerHTML = this.getUsersContent(); this.setupUsersRowListeners(); this.setupAddButtonListeners(); this.setupUsersPagerListeners(); } } else { mainContent.innerHTML = this.renderDashboard(); } this.restoreSearchFocus(); this.setActiveNav(page); } setActiveNav(page) { document.querySelectorAll('[data-nav]').forEach(link => { const isActive = link.dataset.nav === page; link.classList.toggle('border-l-4', isActive); link.classList.toggle('border-blue-600', isActive); link.classList.toggle('bg-slate-200/80', isActive); link.classList.toggle('dark:bg-slate-800', isActive); link.classList.toggle('text-slate-900', isActive); link.classList.toggle('dark:text-slate-50', isActive); link.classList.toggle('font-bold', isActive); }); } isMobileViewport() { return window.matchMedia(`(max-width: ${this.mobileBreakpoint}px)`).matches; } setupResponsiveShell() { const menuBtn = document.getElementById('mobileMenuBtn'); const backdrop = document.getElementById('sidebarBackdrop'); if (menuBtn && !menuBtn.dataset.boundClick) { menuBtn.addEventListener('click', () => this.toggleMobileNav()); menuBtn.dataset.boundClick = 'true'; } if (backdrop && !backdrop.dataset.boundClick) { backdrop.addEventListener('click', () => this.closeMobileNav()); backdrop.dataset.boundClick = 'true'; } document.querySelectorAll('[data-nav]').forEach(link => { if (!link.dataset.boundMobileClose) { link.addEventListener('click', () => { if (this.isMobileViewport()) { this.closeMobileNav(); } }); link.dataset.boundMobileClose = 'true'; } }); if (!this.boundResizeHandler) { this.boundResizeHandler = () => { if (!this.isMobileViewport()) { this.closeMobileNav(); } }; window.addEventListener('resize', this.boundResizeHandler); } if (!this.isMobileViewport()) { this.closeMobileNav(); } } toggleMobileNav() { if (document.body.classList.contains('mobile-nav-open')) { this.closeMobileNav(); return; } this.openMobileNav(); } openMobileNav() { if (!this.isMobileViewport()) return; document.body.classList.add('mobile-nav-open'); const menuBtn = document.getElementById('mobileMenuBtn'); if (menuBtn) { menuBtn.setAttribute('aria-expanded', 'true'); } } closeMobileNav() { document.body.classList.remove('mobile-nav-open'); const menuBtn = document.getElementById('mobileMenuBtn'); if (menuBtn) { menuBtn.setAttribute('aria-expanded', 'false'); } } async fetchApplications() { const res = await fetch(`${this.apiBase}/applications`); const data = await res.json(); if (data.success) { this.applications = data.data; } else { console.error('Load applications failed:', data.message); } } async fetchAccounts() { try { const res = await fetch(`${this.apiBase}/accounts/all`); const data = await res.json(); if (data.success) { this.accounts = data.data; } else { console.error('Load accounts failed:', data.message); } } catch (err) { console.error('Fetch accounts error:', err); } } async fetchUsers() { try { const res = await fetch(`${this.apiBase}/users`); const data = await res.json(); if (data.success) { this.users = data.data; } else { console.error('Load users failed:', data.message); } } catch (err) { console.error('Fetch users error:', err); } } async fetchRoles() { try { const res = await fetch(`${this.apiBase}/roles`); const data = await res.json(); if (data.success) { this.roles = data.data; } else { console.error('Load roles failed:', data.message); } } catch (err) { console.error('Fetch roles error:', err); } } async loadModals() { try { const existingContainer = document.getElementById('modalsContainer'); if (existingContainer && existingContainer.children.length) { return; } const response = await fetch('../modals.html'); const modalsHTML = await response.text(); const container = existingContainer || document.createElement('div'); if (!container.id) container.id = 'modalsContainer'; container.innerHTML = modalsHTML; if (!container.parentElement) { document.body.appendChild(container); } this.setupFormListeners(); this.setupAccountRowListeners(); this.setupAddButtonListeners(); this.setupFilters(); } catch (error) { console.error('Lỗi load modals:', error); } } restoreSearchFocus() { const accountSearch = document.getElementById('accountSearch'); const appSearch = document.getElementById('appSearch'); if (accountSearch && accountSearch.dataset.focused === 'true') { const pos = accountSearch.selectionStart || accountSearch.value.length; accountSearch.focus(); accountSearch.setSelectionRange(pos, pos); } if (appSearch && appSearch.dataset.focused === 'true') { const pos = appSearch.selectionStart || appSearch.value.length; appSearch.focus(); appSearch.setSelectionRange(pos, pos); } } setupEventListeners() { // Modal close buttons document.querySelectorAll('[data-close-modal]').forEach(btn => { btn.addEventListener('click', () => this.closeModals()); }); // Close with Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.closeMobileNav(); this.closeModals(); } }); // Form submissions // Logout button const logoutBtn = document.getElementById('logoutBtn'); if (logoutBtn) { logoutBtn.addEventListener('click', () => this.handleLogout()); } const profileBtn = document.getElementById('profileBtn'); if (profileBtn) { profileBtn.addEventListener('click', () => this.openProfileModal()); } // Update account display this.updateAccountDisplay(); // Account table row clicks this.setupAccountRowListeners(); this.setupFilters(); this.setupResponsiveShell(); } setupFormListeners() { const accountForm = document.getElementById('accountForm'); if (accountForm) { if (!accountForm.dataset.boundSubmit) { accountForm.addEventListener('submit', (e) => this.handleAccountSubmit(e)); accountForm.dataset.boundSubmit = 'true'; } } const appForm = document.getElementById('appForm'); if (appForm) { if (!appForm.dataset.boundSubmit) { appForm.addEventListener('submit', (e) => this.handleAppSubmit(e)); appForm.dataset.boundSubmit = 'true'; } } // Close when clicking backdrop outside modal content document.querySelectorAll('.modal-backdrop').forEach(backdrop => { backdrop.addEventListener('click', (evt) => { if (evt.target === backdrop) { this.closeModals(); } }); }); } updateAccountDisplay() { // Use the logged-in user from constructor const usernameEl = document.getElementById('accountUsername'); const roleEl = document.getElementById('accountRole'); if (usernameEl) usernameEl.textContent = this.currentUser?.username || this.currentUser?.Username || 'User'; if (roleEl) roleEl.textContent = this.getCurrentUserRoleRaw() || 'Guest'; } getFilteredAccounts() { const svcFilter = this.accountServiceFilter || ''; const search = (this.accountSearchTerm || '').toLowerCase(); return this.accounts.filter(acc => { const matchesService = !svcFilter || String(acc.AppId) === String(svcFilter); if (!matchesService) return false; if (!search) return true; const hay = [acc.AccountUsername, acc.Email, acc.AppName, acc.AppType].map(v => (v || '').toLowerCase()); return hay.some(val => val.includes(search)); }); } getFilteredApplications() { const search = (this.applicationSearchTerm || '').toLowerCase(); if (!search) return this.applications; return this.applications.filter(app => { const hay = [app.Name, app.Type, app.Description, app.Url, app.Icon].map(v => (v || '').toLowerCase()); return hay.some(val => val.includes(search)); }); } getPaged(items, page, pageSize) { const total = items.length; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const current = Math.min(Math.max(1, page), totalPages); const start = (current - 1) * pageSize; return { current, total, totalPages, data: items.slice(start, start + pageSize), start: total === 0 ? 0 : start + 1, end: Math.min(total, start + pageSize) }; } maskForeignAccountUsername(username) { const value = String(username || '').trim(); if (!value) return '-'; if (value.length < 5) { return `${value.slice(0, 1)}*****`; } return `${value.slice(0, 3)}*****`; } handleLogout() { if (confirm('Are you sure you want to logout?')) { this.saveToStorage('currentUser', null); localStorage.clear(); window.location.href = '../pages/login.html'; } } renderDashboard() { return `

System Overview

Account & Service Management

Applications
${this.applications.length} ${this.applications.filter(a => (a.Status || a.status) === 'online').length} Active
Total Accounts
${this.accounts.length} Managed
Last Updated
${new Date().toLocaleDateString()}
Status
Operational check_circle

history Recent Accounts

${this.accounts.length > 0 ? `
${this.accounts.slice(-5).reverse().map(acc => { const username = acc.AccountUsername || acc.username || '-'; const service = acc.AppName || acc.service || '-'; const owner = acc.Email || acc.owner || this.currentUser?.Username || this.currentUser?.username || '-'; return `

${username}

${service} • ${owner}

`;}).join('')}
` : `

No accounts yet. Create one

`}
`; } getAccountsContent() { const filteredAccounts = this.getFilteredAccounts(); const currentUserId = this.getUserId(); const pageInfo = this.getPaged(filteredAccounts, this.accountPage, this.accountPageSize); this.accountPage = pageInfo.current; return `
Service
Search
${pageInfo.data.length > 0 ? `
${pageInfo.data.map(acc => { const isOwnAccount = acc.UserId == currentUserId; const accountUsername = acc.AccountUsername || '-'; const displayAccountUsername = isOwnAccount ? accountUsername : this.maskForeignAccountUsername(accountUsername); const actionContent = isOwnAccount ? `` : '-'; return ` `; }).join('')}
Owner Username Service Actions
Showing ${pageInfo.start}-${pageInfo.end} of ${pageInfo.total}
Page ${pageInfo.current} / ${pageInfo.totalPages}
` : `

No accounts yet. Create one to get started.

`}
`; } getApplicationsContent() { const filteredApps = this.getFilteredApplications(); const pageInfo = this.getPaged(filteredApps, this.appPage, this.appPageSize); this.appPage = pageInfo.current; return `
lan

Active

${this.applications.filter(a => a.status === 'online').length}

bolt

Total

${this.applications.length}

database

Health

99.9%

Search
${pageInfo.data.map(app => ` `).join('')}
Name Type Description URL Status Actions
${app.Icon || 'apps'}
${app.Name}
${app.Type} ${app.Description || '-'} ${(app.Url || app.url) ? `${app.Url || app.url}` : '-'}
${(app.Status || app.status) === 'online' ? 'Online' : 'Offline'}
Showing ${pageInfo.start}-${pageInfo.end} of ${pageInfo.total}
Page ${pageInfo.current} / ${pageInfo.totalPages}
`; } renderAccountsTableBody() { const tbody = document.querySelector('.accounts-table-body'); if (!tbody) return; const currentUserId = this.getUserId(); const pageInfo = this.getPaged(this.getFilteredAccounts(), this.accountPage, this.accountPageSize); this.accountPage = pageInfo.current; tbody.innerHTML = pageInfo.data.map(acc => { const isOwnAccount = acc.UserId == currentUserId; const accountUsername = acc.AccountUsername || '-'; const displayAccountUsername = isOwnAccount ? accountUsername : this.maskForeignAccountUsername(accountUsername); const actionContent = isOwnAccount ? ` ` : '-'; return ` ${acc.Email || '-'} ${displayAccountUsername} ${acc.AppName || '-'} ${actionContent} `; }).join(''); const pager = document.getElementById('accountsPager'); if (pager) { pager.innerHTML = ` Showing ${pageInfo.start}-${pageInfo.end} of ${pageInfo.total}
Page ${pageInfo.current} / ${pageInfo.totalPages}
`; } this.setupAccountRowListeners(); this.setupAccountPagerListeners(); } renderApplicationsTableBody() { const tbody = document.querySelector('.apps-table-body'); if (!tbody) return; const pageInfo = this.getPaged(this.getFilteredApplications(), this.appPage, this.appPageSize); this.appPage = pageInfo.current; tbody.innerHTML = pageInfo.data.map(app => `
${app.Icon || 'apps'}
${app.Name}
${app.Type} ${app.Description || '-'} ${(app.Url || app.url) ? `${app.Url || app.url}` : '-'}
${(app.Status || app.status) === 'online' ? 'Online' : 'Offline'}
`).join(''); const pager = document.getElementById('appsPager'); if (pager) { pager.innerHTML = ` Showing ${pageInfo.start}-${pageInfo.end} of ${pageInfo.total}
Page ${pageInfo.current} / ${pageInfo.totalPages}
`; } this.setupAccountRowListeners(); this.setupAppPagerListeners(); } setupAccountPagerListeners() { document.querySelectorAll('.account-page-btn').forEach(btn => { btn.addEventListener('click', () => { const targetPage = Number(btn.dataset.page); if (!targetPage || targetPage < 1) return; this.accountPage = targetPage; this.renderAccountsTableBody(); }); }); } setupAppPagerListeners() { document.querySelectorAll('.app-page-btn').forEach(btn => { btn.addEventListener('click', () => { const targetPage = Number(btn.dataset.page); if (!targetPage || targetPage < 1) return; this.appPage = targetPage; this.renderApplicationsTableBody(); }); }); } setupAccountRowListeners() { // View Account listeners document.querySelectorAll('.view-account').forEach(btn => { btn.addEventListener('click', (e) => { if (btn.disabled) return; // Only view own accounts const accountId = Number(btn.dataset.accountId); const account = this.accounts.find(a => a.AccountId === accountId); this.currentViewAccountId = accountId; this.currentViewAccount = account; document.getElementById('viewAccountService').textContent = account?.AppName || '-'; document.getElementById('viewAccountOwner').textContent = account?.Email || '-'; document.getElementById('viewAccountUsername').textContent = account?.AccountUsername || '-'; const passwordEl = document.getElementById('viewAccountPassword'); const toggleBtn = document.querySelector('.toggle-password'); const toggleIcon = document.getElementById('toggleIcon'); const storedPwd = account?.AccountPassword || ''; passwordEl.dataset.password = storedPwd; passwordEl.textContent = storedPwd ? '••••••••' : '(no password stored)'; passwordEl.dataset.visible = 'false'; if (toggleIcon) toggleIcon.textContent = 'visibility'; // Rebind toggle each time modal opens to keep state fresh if (toggleBtn) { toggleBtn.onclick = () => { const currentPwd = passwordEl.dataset.password || ''; const isVisible = passwordEl.dataset.visible === 'true'; if (isVisible) { passwordEl.textContent = currentPwd ? '••••••••' : '(no password stored)'; passwordEl.dataset.visible = 'false'; if (toggleIcon) toggleIcon.textContent = 'visibility'; } else { passwordEl.textContent = currentPwd || '(no password stored)'; passwordEl.dataset.visible = 'true'; if (toggleIcon) toggleIcon.textContent = 'visibility_off'; } }; } document.getElementById('viewAccountModal').classList.add('open'); }); }); // Delete Account listeners - show confirmation modal document.querySelectorAll('.delete-account').forEach(btn => { btn.addEventListener('click', (e) => { if (btn.disabled) return; // Don't delete others' accounts const accountId = Number(btn.dataset.accountId); const account = this.accounts.find(a => a.AccountId === accountId); this.pendingDeleteAccountId = accountId; document.getElementById('deleteAccountUsername').textContent = account?.AccountUsername || ''; document.getElementById('deleteAccountModal').classList.add('open'); }); }); // Confirm Delete Account document.querySelectorAll('.confirm-delete-account').forEach(btn => { btn.addEventListener('click', () => { if (this.pendingDeleteAccountId !== undefined) { fetch(`${this.apiBase}/accounts/${this.pendingDeleteAccountId}`, { method: 'DELETE' }) .then(res => res.json()) .then(data => { if (data.success) { this.notifySuccess('Account deleted successfully'); this.closeModals(); this.refreshAccountsUI(); } else { this.notifyFailure(data.message || 'Delete account failed'); } }) .catch(err => { console.error(err); this.notifyFailure('Delete account failed'); }); } }); }); // Edit Account listeners document.querySelectorAll('.edit-account').forEach(btn => { btn.addEventListener('click', (e) => { if (btn.disabled) return; // Don't edit others' accounts const accountId = Number(btn.dataset.accountId); const account = this.accounts.find(a => a.AccountId === accountId); // Populate form with existing data const form = document.getElementById('accountForm'); if (form) { const userInput = form.querySelector('#accountUsername'); const passInput = form.querySelector('#accountPassword'); const ownerInput = form.querySelector('#accountOwner'); const serviceSelect = form.querySelector('#accountService'); if (userInput) userInput.value = account?.AccountUsername || ''; if (passInput) passInput.value = account?.AccountPassword || ''; if (ownerInput) ownerInput.value = this.currentUser?.Username || this.currentUser?.username || ''; if (serviceSelect) serviceSelect.value = account?.AppId || ''; } this.pendingAccountAppId = account?.AppId; this.editingAccountId = account?.AccountId; this.closeModals(); this.openAccountModal(); }); }); // Edit from View modal document.querySelectorAll('.edit-account-from-view').forEach(btn => { btn.addEventListener('click', () => { const account = this.currentViewAccount; const form = document.getElementById('accountForm'); if (form) { const userInput = form.querySelector('#accountUsername'); const passInput = form.querySelector('#accountPassword'); const ownerInput = form.querySelector('#accountOwner'); const serviceSelect = form.querySelector('#accountService'); if (userInput) userInput.value = account?.AccountUsername || ''; if (passInput) passInput.value = account?.AccountPassword || ''; if (ownerInput) ownerInput.value = this.currentUser?.Username || this.currentUser?.username || ''; if (serviceSelect) serviceSelect.value = account?.AppId || ''; } this.pendingAccountAppId = account?.AppId; this.editingAccountId = account?.AccountId; this.closeModals(); this.openAccountModal(); }); }); // View App listeners document.querySelectorAll('.view-app').forEach(btn => { btn.addEventListener('click', (e) => { const appId = Number(btn.dataset.appId); const app = this.applications.find(a => a.AppId === appId); this.currentViewAppId = appId; document.getElementById('viewAppName').textContent = app?.Name || '-'; document.getElementById('viewAppType').textContent = app?.Type || '-'; const iconVal = app?.Icon || app?.icon || 'apps'; const iconSymbolEl = document.getElementById('viewAppIconSymbol'); const iconNameEl = document.getElementById('viewAppIconName'); if (iconSymbolEl) iconSymbolEl.textContent = iconVal; if (iconNameEl) iconNameEl.textContent = iconVal; document.getElementById('viewAppDescription').textContent = app?.Description || '-'; const urlEl = document.getElementById('viewAppUrl'); const urlVal = app?.Url || app?.url; if (urlEl) { if (urlVal) { urlEl.innerHTML = `${urlVal}`; } else { urlEl.textContent = '-'; } } const statusValue = app?.Status || app?.status; document.getElementById('viewAppStatus').textContent = statusValue === 'online' ? 'Online' : 'Offline'; document.getElementById('viewAppModal').classList.add('open'); }); }); // Delete App listeners - show confirmation modal document.querySelectorAll('.delete-app').forEach(btn => { btn.addEventListener('click', (e) => { const appId = Number(btn.dataset.appId); const app = this.applications.find(a => a.AppId === appId); this.pendingDeleteAppId = appId; document.getElementById('deleteAppName').textContent = app?.Name || ''; document.getElementById('deleteAppModal').classList.add('open'); }); }); // Confirm Delete App document.querySelectorAll('.confirm-delete-app').forEach(btn => { btn.addEventListener('click', () => { if (this.pendingDeleteAppId !== undefined) { fetch(`${this.apiBase}/applications/${this.pendingDeleteAppId}`, { method: 'DELETE' }) .then(res => res.json()) .then(data => { if (data.success) { this.notifySuccess('Application deleted successfully'); this.closeModals(); this.refreshApplicationsUI(); } else { this.notifyFailure(data.message || 'Delete application failed'); } }) .catch(err => { console.error(err); this.notifyFailure('Delete application failed'); }); } }); }); // Edit App listeners document.querySelectorAll('.edit-app').forEach(btn => { btn.addEventListener('click', (e) => { const appId = Number(btn.dataset.appId); const app = this.applications.find(a => a.AppId === appId); document.getElementById('appName').value = app?.Name || ''; document.getElementById('appType').value = app?.Type || ''; document.getElementById('appStatus').value = app?.Status || 'online'; document.getElementById('appDescription').value = app?.Description || ''; document.getElementById('appIcon').value = app?.Icon || app?.icon || ''; document.getElementById('appUrl').value = app?.Url || app?.url || ''; this.editingAppId = app?.AppId; this.closeModals(); this.openAppModal(); }); }); // Edit App from View modal document.querySelectorAll('.edit-app-from-view').forEach(btn => { btn.addEventListener('click', () => { const appId = this.currentViewAppId; const app = this.applications.find(a => a.AppId === appId); document.getElementById('appName').value = app?.Name || ''; document.getElementById('appType').value = app?.Type || ''; document.getElementById('appStatus').value = app?.Status || 'online'; document.getElementById('appDescription').value = app?.Description || ''; document.getElementById('appIcon').value = app?.Icon || app?.icon || ''; document.getElementById('appUrl').value = app?.Url || ''; this.editingAppId = app?.AppId; this.closeModals(); this.openAppModal(); }); }); } setupAddButtonListeners() { // Add Account button document.querySelectorAll('#addAccountBtn').forEach(btn => { btn.addEventListener('click', () => { this.editingAccountId = undefined; this.pendingAccountAppId = undefined; this.openAccountModal(); }); }); // Add Application button document.querySelectorAll('#addAppBtn').forEach(btn => { btn.addEventListener('click', () => { this.editingAppId = undefined; this.openAppModal(); }); }); } setupFilters() { const serviceFilter = document.getElementById('serviceFilter'); if (serviceFilter) { serviceFilter.value = this.accountServiceFilter || ''; serviceFilter.addEventListener('change', (e) => { this.accountServiceFilter = e.target.value; this.renderAccountsTableBody(); }); } const accountSearch = document.getElementById('accountSearch'); if (accountSearch) { accountSearch.value = this.accountSearchTerm; const handleAccountSearch = event => { this.accountSearchTerm = event.target.value.toLowerCase(); this.renderAccountsTableBody(); }; accountSearch.addEventListener('input', handleAccountSearch); // Restore focus/selection after renders to avoid typing interruptions accountSearch.addEventListener('focus', () => { accountSearch.dataset.focused = 'true'; }); accountSearch.addEventListener('blur', () => { accountSearch.dataset.focused = 'false'; }); } const appSearch = document.getElementById('appSearch'); if (appSearch) { appSearch.value = this.applicationSearchTerm; const handleApplicationSearch = event => { this.applicationSearchTerm = event.target.value.toLowerCase(); this.renderApplicationsTableBody(); }; appSearch.addEventListener('input', handleApplicationSearch); // Restore focus/selection after renders to avoid typing interruptions appSearch.addEventListener('focus', () => { appSearch.dataset.focused = 'true'; }); appSearch.addEventListener('blur', () => { appSearch.dataset.focused = 'false'; }); } } async handleAccountSubmit(e) { e.preventDefault(); const accountForm = document.getElementById('accountForm'); const userId = this.getUserId(); const appId = Number(accountForm?.querySelector('#accountService')?.value || 0); const accountUsername = (accountForm?.querySelector('#accountUsername')?.value || '').trim(); const accountPassword = (accountForm?.querySelector('#accountPassword')?.value || '').trim(); const accountEmail = ((accountForm?.querySelector('#accountOwner')?.value || '').trim()) || this.currentUser?.Username || this.currentUser?.username || ''; if (!accountForm) { this.notifyFailure('Account form not found.'); return; } if (!userId) { this.notifyFailure('User is not authenticated. Please login again.'); return; } if (!appId) { this.notifyWarning('Please select a service.'); return; } if (!accountUsername) { this.notifyWarning('Please enter a username.'); return; } if (!accountPassword) { this.notifyWarning('Please enter a password.'); return; } const payload = { userId, appId, accountUsername, accountPassword, email: accountEmail, accessLevel: 'user', notes: '' }; const isEdit = this.editingAccountId !== undefined; const url = isEdit ? `${this.apiBase}/accounts/${this.editingAccountId}` : `${this.apiBase}/accounts`; const method = isEdit ? 'PUT' : 'POST'; fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(res => res.json()).then(data => { if (data.success) { this.editingAccountId = undefined; this.pendingAccountAppId = undefined; this.notifySuccess(isEdit ? 'Account updated successfully' : 'Account created successfully'); this.closeModals(); this.refreshAccountsUI(); } else { this.notifyFailure(data.message || 'Save account failed'); } }).catch(err => { console.error(err); this.notifyFailure('Save account failed'); }); } async refreshAccountsUI() { await this.fetchAccounts(); if (this.currentPage === 'accounts') { this.renderView('accounts'); } } async handleAppSubmit(e) { e.preventDefault(); const payload = { name: document.getElementById('appName').value, type: document.getElementById('appType').value, status: document.getElementById('appStatus').value, icon: (document.getElementById('appIcon')?.value || 'apps').trim() || 'apps', description: document.getElementById('appDescription')?.value || '', url: (document.getElementById('appUrl')?.value || '').trim() }; const isEdit = this.editingAppId !== undefined; const url = isEdit ? `${this.apiBase}/applications/${this.editingAppId}` : `${this.apiBase}/applications`; const method = isEdit ? 'PUT' : 'POST'; fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(res => res.json()).then(data => { if (data.success) { this.editingAppId = undefined; this.notifySuccess(isEdit ? 'Application updated successfully' : 'Application created successfully'); this.closeModals(); this.refreshApplicationsUI(); } else { this.notifyFailure(data.message || 'Save application failed'); } }).catch(err => { console.error(err); this.notifyFailure('Save application failed'); }); } async refreshApplicationsUI() { await this.fetchApplications(); if (this.currentPage === 'applications') { this.renderView('applications'); } } openAccountModal() { // Refresh service options so newly added applications appear const serviceSelect = document.getElementById('accountService'); if (serviceSelect) { serviceSelect.innerHTML = `` + this.applications.map(app => ``).join(''); if (this.editingAccountId !== undefined && this.pendingAccountAppId) { serviceSelect.value = this.pendingAccountAppId; } } if (this.editingAccountId === undefined) { const form = document.getElementById('accountForm'); if (form) { const serviceSelect = form.querySelector('#accountService'); const ownerInput = form.querySelector('#accountOwner'); const userInput = form.querySelector('#accountUsername'); const passInput = form.querySelector('#accountPassword'); if (serviceSelect) serviceSelect.value = ''; if (ownerInput) ownerInput.value = this.currentUser?.Username || this.currentUser?.username || ''; if (userInput) userInput.value = ''; if (passInput) passInput.value = ''; } } document.getElementById('accountModal').classList.add('open'); } openAppModal() { if (this.editingAppId === undefined) { document.getElementById('appName').value = ''; document.getElementById('appType').value = ''; document.getElementById('appStatus').value = 'online'; const iconInput = document.getElementById('appIcon'); const desc = document.getElementById('appDescription'); const url = document.getElementById('appUrl'); if (iconInput) iconInput.value = ''; if (desc) desc.value = ''; if (url) url.value = ''; } document.getElementById('appModal').classList.add('open'); } closeModals() { document.querySelectorAll('.modal-backdrop').forEach(modal => { modal.classList.remove('open'); }); } async openProfileModal() { try { const response = await fetch(`${this.apiBase}/users/me`, { headers: this.getAuthHeaders(false) }); const data = await response.json(); if (!data.success || !data.data) { this.notifyFailure(data.message || 'Cannot load profile'); return; } this.renderProfileModal(data.data); } catch (err) { console.error(err); this.notifyFailure('Cannot load profile'); } } renderProfileModal(profile) { const isVerified = profile?.EmailVerified === true || profile?.EmailVerified === 1; const html = ` `; const containerId = 'profileModalContainer'; let container = document.getElementById(containerId); if (!container) { container = document.createElement('div'); container.id = containerId; document.body.appendChild(container); } container.innerHTML = html; const form = document.getElementById('profileForm'); if (form) { form.addEventListener('submit', (e) => this.saveProfile(e)); } this.setupProfilePasswordToggles(); const modal = document.getElementById('profileModal'); if (modal) { modal.addEventListener('click', (e) => { if (e.target === modal) { closeProfileModal(); } }); } } setupProfilePasswordToggles() { document.querySelectorAll('[data-password-toggle]').forEach((toggleBtn) => { if (toggleBtn.dataset.bound === 'true') { return; } toggleBtn.addEventListener('click', () => { const inputId = toggleBtn.dataset.passwordToggle; if (!inputId) return; const input = document.getElementById(inputId); const icon = document.getElementById(`${inputId}Icon`); if (!input) return; const isHidden = input.type === 'password'; input.type = isHidden ? 'text' : 'password'; if (icon) { icon.textContent = isHidden ? 'visibility_off' : 'visibility'; } }); toggleBtn.dataset.bound = 'true'; }); } async saveProfile(e) { e.preventDefault(); const fullname = document.getElementById('profileFullName')?.value.trim() || ''; const email = document.getElementById('profileEmail')?.value.trim() || ''; const currentPassword = document.getElementById('profileCurrentPassword')?.value || ''; const newPassword = document.getElementById('profileNewPassword')?.value || ''; const confirmPassword = document.getElementById('profileConfirmPassword')?.value || ''; if (!fullname || !email) { this.notifyFailure('Full name and email are required'); return; } if (newPassword && newPassword !== confirmPassword) { this.notifyFailure('New password and confirm password do not match'); return; } if (newPassword && !currentPassword) { this.notifyFailure('Current password is required to change password'); return; } try { const response = await fetch(`${this.apiBase}/users/me`, { method: 'PUT', headers: this.getAuthHeaders(true), body: JSON.stringify({ fullname, email, currentPassword, newPassword }) }); const data = await response.json(); if (!data.success) { this.notifyFailure(data.message || 'Update profile failed'); return; } if (data.user) { this.currentUser = { ...this.currentUser, ...data.user, role: data.user.role || data.user.Role || this.currentUser.role || this.currentUser.Role }; this.saveToStorage('currentUser', this.currentUser); this.updateAccountDisplay(); } closeProfileModal(); this.notifySuccess(data.message || 'Profile updated'); if (data.verificationRequired && data.emailSent === false) { if (data.verificationPreviewUrl) { this.notifyWarning(`Email confirmation link (dev): ${data.verificationPreviewUrl}`); } else { this.notifyWarning('Email changed but verification email could not be sent.'); } } } catch (err) { console.error(err); this.notifyFailure('Update profile failed'); } } loadFromStorage(key) { const data = localStorage.getItem(key); return data ? JSON.parse(data) : null; } saveToStorage(key, data) { localStorage.setItem(key, JSON.stringify(data)); } formatDateTime(value) { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) { return String(value); } return date.toLocaleString(); } // ========== Users Management ========== getUsersContent() { const filteredUsers = this.getFilteredUsers(); const pageInfo = this.getPaged(filteredUsers, this.userPage, this.userPageSize); this.userPage = pageInfo.current; return `
search
${pageInfo.data.length === 0 ? ` ` : pageInfo.data.map(user => ` `).join('')}
Username Full Name Email Role Status Actions
No users found
${user.Username} ${user.FullName || '-'} ${user.Email || '-'} ${user.RoleName || user.Role || 'N/A'} ${user.IsActive ? 'Active' : 'Inactive'}
Showing ${pageInfo.start}-${pageInfo.end} of ${pageInfo.total}
Page ${pageInfo.current} / ${pageInfo.totalPages}
`; } setupUsersRowListeners() { const userRows = document.querySelectorAll('.user-row'); userRows.forEach(row => { const viewBtn = row.querySelector('.view-user-btn'); const editBtn = row.querySelector('.edit-user-btn'); const deleteBtn = row.querySelector('.delete-user-btn'); const userId = row.dataset.userId; if (viewBtn) { viewBtn.addEventListener('click', () => this.viewUserDetails(userId)); } if (editBtn) { editBtn.addEventListener('click', () => this.editUser(userId)); } if (deleteBtn && !deleteBtn.disabled) { deleteBtn.addEventListener('click', () => this.deleteUserConfirm(userId)); } }); // Search and Filter const searchInput = document.getElementById('userSearch'); const roleFilter = document.getElementById('roleFilter'); if (searchInput) { searchInput.oninput = (e) => { this.userSearchTerm = (e.target.value || '').toLowerCase(); this.userPage = 1; this.renderUsersTableBody(); }; } if (roleFilter) { roleFilter.value = this.userRoleFilter || ''; roleFilter.onchange = (e) => { this.userRoleFilter = e.target.value; this.userPage = 1; this.renderUsersTableBody(); }; } // Add User Button const addBtn = document.getElementById('addUserBtn'); if (addBtn) { addBtn.onclick = () => this.openUserModal(); } const addRoleBtn = document.getElementById('addRoleBtn'); if (addRoleBtn) { addRoleBtn.onclick = () => this.openRoleModal(); } } getFilteredUsers() { const search = (this.userSearchTerm || '').toLowerCase(); const roleId = this.userRoleFilter || ''; return this.users.filter(user => { const matchesSearch = !search || [user.Username, user.FullName, user.Email].some(val => (val || '').toLowerCase().includes(search)); const matchesRole = !roleId || String(user.RoleId) === String(roleId) || String(user.RoleID) === String(roleId); return matchesSearch && matchesRole; }); } renderUsersTableBody() { const tbody = document.querySelector('.users-table-body'); if (!tbody) return; const pageInfo = this.getPaged(this.getFilteredUsers(), this.userPage, this.userPageSize); this.userPage = pageInfo.current; tbody.innerHTML = pageInfo.data.length === 0 ? ` No users found ` : pageInfo.data.map(user => ` ${user.Username} ${user.FullName || '-'} ${user.Email || '-'} ${user.RoleName || user.Role || 'N/A'} ${user.IsActive ? 'Active' : 'Inactive'}
`).join(''); const pager = document.getElementById('usersPager'); if (pager) { pager.innerHTML = ` Showing ${pageInfo.start}-${pageInfo.end} of ${pageInfo.total}
Page ${pageInfo.current} / ${pageInfo.totalPages}
`; } this.setupUsersRowListeners(); this.setupUsersPagerListeners(); } setupUsersPagerListeners() { document.querySelectorAll('.user-page-btn').forEach(btn => { btn.addEventListener('click', () => { const targetPage = Number(btn.dataset.page); if (!targetPage || targetPage < 1) return; this.userPage = targetPage; this.renderUsersTableBody(); }); }); } openUserModal() { this.showUserFormModal(null); } openRoleModal() { this.showRoleFormModal(); } showRoleFormModal() { const html = ` `; const editingContainer = document.getElementById('roleModalContainer'); if (editingContainer) { editingContainer.innerHTML = html; } else { const container = document.createElement('div'); container.id = 'roleModalContainer'; document.body.appendChild(container); container.innerHTML = html; } const form = document.getElementById('roleForm'); if (form) { form.addEventListener('submit', (e) => this.saveRole(e)); } const modal = document.getElementById('roleModal'); if (modal) { modal.addEventListener('click', function(e) { if (e.target === this) { closeRoleModal(); } }); } } async saveRole(e) { e.preventDefault(); const roleName = document.getElementById('roleName')?.value.trim(); const description = document.getElementById('roleDescription')?.value.trim(); if (!roleName) { this.notifyFailure('Role name is required'); return; } const roleExists = this.roles.some(role => String(role.RoleName || '').trim().toLowerCase() === roleName.toLowerCase() ); if (roleExists) { this.notifyWarning('Role already exists'); return; } try { const response = await fetch(`${this.apiBase}/roles`, { method: 'POST', headers: this.getAuthHeaders(true), body: JSON.stringify({ roleName, description: description || null }) }); const data = await response.json(); if (!response.ok || !data.success) { this.notifyFailure(data.message || 'Create role failed'); return; } this.notifySuccess('Role created'); closeRoleModal(); await this.fetchRoles(); if (this.currentPage === 'users') { this.renderView('users'); } } catch (err) { console.error(err); this.notifyFailure('Create role failed'); } } showUserFormModal(userId) { const user = userId ? this.users.find(u => u.UserId == userId) : null; const html = ` `; // Insert modal in DOM const editingContainer = document.getElementById('userModalContainer'); if (editingContainer) { editingContainer.innerHTML = html; } else { const container = document.createElement('div'); container.id = 'userModalContainer'; document.body.appendChild(container); container.innerHTML = html; } // Add form submit listener const form = document.getElementById('userForm'); if (form) { form.addEventListener('submit', (e) => this.saveUser(e, userId)); } const passwordInput = document.getElementById('userPassword'); const passwordToggleBtn = document.getElementById('userPasswordToggle'); const passwordToggleIcon = document.getElementById('userPasswordToggleIcon'); if (passwordInput && passwordToggleBtn) { passwordToggleBtn.addEventListener('click', () => { const isHidden = passwordInput.type === 'password'; passwordInput.type = isHidden ? 'text' : 'password'; if (passwordToggleIcon) { passwordToggleIcon.textContent = isHidden ? 'visibility_off' : 'visibility'; } }); } // Close on backdrop click const modal = document.getElementById('userModal'); if (modal) { modal.addEventListener('click', function(e) { if (e.target === this) { closeUserModal(); } }); } } async saveUser(e, userId) { e.preventDefault(); const username = document.getElementById('userUsername').value.trim(); const fullname = document.getElementById('userFullName').value.trim(); const email = document.getElementById('userEmail').value.trim(); const password = document.getElementById('userPassword')?.value.trim(); const roleId = document.getElementById('userRole').value; const isActive = document.getElementById('userActive').checked; // Validate required fields if (!username || !fullname) { this.notifyFailure('Username and Full Name are required'); return; } // Password required for new user if (!userId && !password) { this.notifyFailure('Password is required for new user'); return; } const method = userId ? 'PUT' : 'POST'; const url = userId ? `${this.apiBase}/users/${userId}` : `${this.apiBase}/users`; const payload = userId ? { email: email || null, fullname, roleId: parseInt(roleId), status: 'Active', isActive, ...(password ? { password } : {}) } : { username, password, email: email || null, fullname, roleId: parseInt(roleId) }; try { const response = await fetch(url, { method, headers: this.getAuthHeaders(true), body: JSON.stringify(payload) }); const data = await response.json(); if (data.success) { this.notifySuccess(userId ? 'User updated' : 'User created'); closeUserModal(); this.refreshUsersUI(); } else { this.notifyFailure(data.message || 'Save failed'); } } catch (err) { console.error(err); this.notifyFailure('Save failed'); } } async editUser(userId) { this.showUserFormModal(userId); } async viewUserDetails(userId) { try { const response = await fetch(`${this.apiBase}/users/${userId}`, { headers: this.getAuthHeaders(false) }); const data = await response.json(); if (!data.success || !data.data) { this.notifyFailure(data.message || 'Cannot load user details'); return; } this.showUserDetailsModal(data.data); } catch (err) { console.error(err); this.notifyFailure('Cannot load user details'); } } showUserDetailsModal(user) { const html = ` `; const detailsContainer = document.getElementById('userDetailsModalContainer'); if (detailsContainer) { detailsContainer.innerHTML = html; } else { const container = document.createElement('div'); container.id = 'userDetailsModalContainer'; container.innerHTML = html; document.body.appendChild(container); } const passwordValue = user?.Password || ''; const hasReadablePassword = user?.PasswordAvailable === true || Boolean(passwordValue); const usernameEl = document.getElementById('userDetailUsername'); const fullNameEl = document.getElementById('userDetailFullName'); const emailEl = document.getElementById('userDetailEmail'); const roleEl = document.getElementById('userDetailRole'); const statusEl = document.getElementById('userDetailStatus'); const createdDateEl = document.getElementById('userDetailCreatedDate'); const lastLoginEl = document.getElementById('userDetailLastLogin'); const passwordEl = document.getElementById('userDetailPassword'); const passwordToggleBtn = document.getElementById('userDetailPasswordToggle'); const passwordToggleIcon = document.getElementById('userDetailPasswordToggleIcon'); const editBtn = document.getElementById('userDetailEditBtn'); const detailsModal = document.getElementById('userDetailsModal'); if (usernameEl) usernameEl.textContent = user?.Username || '-'; if (fullNameEl) fullNameEl.textContent = user?.FullName || '-'; if (emailEl) emailEl.textContent = user?.Email || '-'; if (roleEl) roleEl.textContent = user?.RoleName || user?.Role || '-'; if (statusEl) statusEl.textContent = user?.IsActive ? 'Active' : 'Inactive'; if (createdDateEl) createdDateEl.textContent = this.formatDateTime(user?.CreatedDate); if (lastLoginEl) lastLoginEl.textContent = this.formatDateTime(user?.LastLogin); if (passwordEl) { passwordEl.dataset.password = passwordValue; passwordEl.dataset.visible = 'false'; passwordEl.textContent = hasReadablePassword ? '••••••••' : '(khong the hien thi - can reset password 1 lan)'; } if (passwordToggleBtn && passwordEl) { passwordToggleBtn.addEventListener('click', () => { if (!hasReadablePassword) { return; } const isVisible = passwordEl.dataset.visible === 'true'; if (isVisible) { passwordEl.textContent = '••••••••'; passwordEl.dataset.visible = 'false'; if (passwordToggleIcon) passwordToggleIcon.textContent = 'visibility'; } else { passwordEl.textContent = passwordValue; passwordEl.dataset.visible = 'true'; if (passwordToggleIcon) passwordToggleIcon.textContent = 'visibility_off'; } }); } if (passwordToggleBtn && !hasReadablePassword) { passwordToggleBtn.disabled = true; passwordToggleBtn.classList.add('opacity-50', 'cursor-not-allowed'); } if (editBtn) { editBtn.addEventListener('click', () => { closeUserDetailsModal(); this.editUser(user?.UserId); }); } if (detailsModal) { detailsModal.addEventListener('click', function(e) { if (e.target === this) { closeUserDetailsModal(); } }); } } async deleteUserConfirm(userId) { const user = this.users.find(u => u.UserId == userId); if (!user) return; if (confirm(`Are you sure you want to delete user "${user.Username}"?`)) { await this.deleteUser(userId); } } async deleteUser(userId) { try { const response = await fetch(`${this.apiBase}/users/${userId}`, { method: 'DELETE', headers: this.getAuthHeaders(false) }); const data = await response.json(); if (data.success) { this.notifySuccess('User deleted'); this.refreshUsersUI(); } else { this.notifyFailure(data.message || 'Delete failed'); } } catch (err) { console.error(err); this.notifyFailure('Delete failed'); } } async refreshUsersUI() { await this.fetchUsers(); if (this.currentPage === 'users') { this.renderView('users'); } } } // Global modal close functions function closeAllModals() { document.querySelectorAll('.modal-backdrop').forEach(modal => { modal.classList.remove('open'); }); } function closeAccountModal() { document.getElementById('accountModal').classList.remove('open'); } function closeViewAccountModal() { document.getElementById('viewAccountModal').classList.remove('open'); } function closeDeleteAccountModal() { document.getElementById('deleteAccountModal').classList.remove('open'); } function closeAppModal() { document.getElementById('appModal').classList.remove('open'); } function closeViewAppModal() { document.getElementById('viewAppModal').classList.remove('open'); } function closeDeleteAppModal() { document.getElementById('deleteAppModal').classList.remove('open'); } function closeUserModal() { const userModalContainer = document.getElementById('userModalContainer'); if (userModalContainer) { userModalContainer.innerHTML = ''; } } function closeRoleModal() { const roleModalContainer = document.getElementById('roleModalContainer'); if (roleModalContainer) { roleModalContainer.innerHTML = ''; } } function closeUserDetailsModal() { const detailsContainer = document.getElementById('userDetailsModalContainer'); if (detailsContainer) { detailsContainer.innerHTML = ''; } } function closeProfileModal() { const profileContainer = document.getElementById('profileModalContainer'); if (profileContainer) { profileContainer.innerHTML = ''; } } // Initialize app when DOM is ready let app; document.addEventListener('DOMContentLoaded', () => { app = new AccountManager(); });