Compare commits

..

2 Commits

Author SHA1 Message Date
0c1ca1d923 dialog 2026-03-31 10:20:35 +07:00
209b68c3f5 save 2026-03-31 10:20:27 +07:00
12 changed files with 1283 additions and 196 deletions

2
.env
View File

@@ -6,7 +6,7 @@ PORT=3000
# SQL Server Configuration # SQL Server Configuration
DB_SERVER=172.20.235.176 DB_SERVER=172.20.235.176
DB_USER=sa DB_USER=sa
DB_PASSWORD=robotics@2020 DB_PASSWORD=robotics@2022
DB_NAME=AccManager DB_NAME=AccManager
DB_ENCRYPT=true DB_ENCRYPT=true
DB_TRUST_CERTIFICATE=true DB_TRUST_CERTIFICATE=true

270
codedialog.html Normal file
View File

@@ -0,0 +1,270 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>Sentinel Accounts - Account Details</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&amp;family=Inter:wght@400;500;600&amp;display=swap"
rel="stylesheet" />
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap"
rel="stylesheet" />
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap"
rel="stylesheet" />
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"on-error-container": "#752121",
"background": "#f7f9fb",
"secondary-dim": "#465468",
"tertiary-dim": "#54506b",
"outline": "#717c82",
"on-secondary-container": "#455367",
"error-dim": "#4e0309",
"primary-fixed": "#dde1ff",
"on-primary-fixed": "#0732a3",
"surface-variant": "#d9e4ea",
"primary-dim": "#2848b7",
"on-tertiary": "#fcf7ff",
"surface": "#f7f9fb",
"primary-container": "#dde1ff",
"inverse-on-surface": "#9a9d9f",
"error": "#9f403d",
"on-primary": "#f8f7ff",
"on-surface-variant": "#566166",
"tertiary-container": "#e3dbfd",
"surface-container-lowest": "#ffffff",
"surface-dim": "#cfdce3",
"tertiary-fixed-dim": "#d4cdee",
"inverse-surface": "#0b0f10",
"on-primary-fixed-variant": "#3352c0",
"surface-tint": "#3755c3",
"secondary-fixed": "#d5e3fc",
"secondary-fixed-dim": "#c7d5ed",
"secondary": "#526074",
"tertiary-fixed": "#e3dbfd",
"on-secondary-fixed-variant": "#4e5c71",
"on-primary-container": "#2747b6",
"primary-fixed-dim": "#cad2ff",
"surface-container-highest": "#d9e4ea",
"surface-container-low": "#f0f4f7",
"surface-container": "#e8eff3",
"on-tertiary-fixed-variant": "#5b5672",
"secondary-container": "#d5e3fc",
"inverse-primary": "#6d89fa",
"outline-variant": "#a9b4b9",
"on-secondary-fixed": "#324053",
"on-surface": "#2a3439",
"surface-container-high": "#e1e9ee",
"on-background": "#2a3439",
"on-tertiary-fixed": "#3e3a54",
"primary": "#3755c3",
"on-tertiary-container": "#514d68",
"on-error": "#fff7f6",
"on-secondary": "#f8f7ff",
"tertiary": "#605c78",
"error-container": "#fe8983",
"surface-bright": "#f7f9fb"
},
fontFamily: {
"headline": ["Manrope"],
"body": ["Inter"],
"label": ["Inter"]
},
borderRadius: { "DEFAULT": "0.125rem", "lg": "0.25rem", "xl": "0.5rem", "full": "0.75rem" },
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
body {
font-family: 'Inter', sans-serif;
}
h1,
h2,
h3 {
font-family: 'Manrope', sans-serif;
}
.glass-overlay {
backdrop-blur: 12px;
background: rgba(247, 249, 251, 0.8);
}
</style>
</head>
<body class="bg-background text-on-surface overflow-hidden h-screen flex flex-col">
<!-- Blurred Background (Mocking the Accounts Management Table) -->
<div class="fixed inset-0 z-0 blur-[8px] opacity-40 pointer-events-none select-none overflow-hidden flex flex-col">
<!-- Mock TopNavBar (From JSON Guidance) -->
<header
class="flex justify-between items-center w-full px-8 h-16 bg-[#f7f9fb] text-[#5a6a72] font-['Manrope'] text-sm tracking-wide font-medium">
<div class="text-lg font-extrabold tracking-tighter text-[#2a3439]">Sentinel Accounts</div>
<div class="flex items-center gap-6">
<span class="material-symbols-outlined">notifications</span>
<span class="material-symbols-outlined">help_outline</span>
</div>
</header>
<div class="flex flex-1">
<!-- Mock SideNavBar (From JSON Guidance) -->
<aside
class="flex flex-col h-full py-6 w-64 bg-[#f0f4f7] text-[#5a6a72] font-['Inter'] text-[0.875rem] font-medium">
<div class="px-6 mb-8">
<div class="text-sm font-black uppercase tracking-[0.05em] text-[#2a3439]">Architectural Sentinel
</div>
<div class="text-xs text-on-surface-variant opacity-70">Enterprise Security</div>
</div>
<nav class="flex-1">
<div
class="flex items-center gap-3 px-6 py-3 bg-white text-[#3755c3] font-bold border-l-4 border-[#3755c3]">
<span class="material-symbols-outlined" data-icon="vpn_key">vpn_key</span> Account Access
</div>
<div class="flex items-center gap-3 px-6 py-3">
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span> Dashboard
</div>
</nav>
</aside>
<!-- Mock Table Stage -->
<main class="flex-1 p-8 bg-background">
<div class="h-12 w-48 bg-surface-container-high rounded-xl mb-6"></div>
<div
class="bg-surface-container-lowest rounded-xl shadow-sm border border-outline-variant/10 overflow-hidden">
<div
class="h-12 border-b border-outline-variant/10 bg-surface-container-low px-6 flex items-center gap-4">
<div class="h-4 w-4 bg-outline-variant/20 rounded"></div>
<div class="h-4 w-24 bg-outline-variant/20 rounded"></div>
<div class="h-4 w-32 bg-outline-variant/20 rounded ml-auto"></div>
</div>
<div class="p-6 space-y-4">
<div class="h-10 w-full bg-surface-container-low/50 rounded-lg"></div>
<div class="h-10 w-full bg-surface-container-low/50 rounded-lg"></div>
<div class="h-10 w-full bg-surface-container-low/50 rounded-lg"></div>
<div class="h-10 w-full bg-surface-container-low/50 rounded-lg"></div>
</div>
</div>
</main>
</div>
</div>
<!-- Modal Overlay -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-on-surface/30 backdrop-blur-[2px]">
<!-- Dialog Container -->
<div
class="w-full max-w-[640px] bg-surface-container-lowest rounded-xl shadow-[0px_12px_32px_rgba(42,52,57,0.12)] border-none overflow-hidden">
<!-- Modal Header -->
<div class="px-8 py-6 border-b border-surface-container flex justify-between items-center">
<div>
<span
class="text-[0.625rem] font-bold uppercase tracking-[0.1em] text-on-surface-variant block mb-1">Resource
Details</span>
<h2 class="text-2xl font-extrabold text-on-surface tracking-tight">Account Details</h2>
</div>
<button
class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-container transition-colors text-on-surface-variant">
<span class="material-symbols-outlined" data-icon="close">close</span>
</button>
</div>
<!-- Modal Content (Bento-style layout for metadata) -->
<div class="p-8 space-y-8">
<!-- Service Info Section -->
<div class="flex items-center gap-5 p-5 bg-surface-container-low rounded-xl">
<div class="w-14 h-14 bg-white rounded-xl shadow-sm flex items-center justify-center">
<span class="material-symbols-outlined text-primary text-3xl" data-icon="cloud">cloud</span>
</div>
<div>
<div class="text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-0.5">Cloud
Infrastructure</div>
<div class="text-xl font-bold text-on-surface">AWS Production</div>
</div>
<div class="ml-auto">
<span
class="px-3 py-1 bg-primary-container text-on-primary-container text-[0.7rem] font-bold rounded-full uppercase tracking-tighter">Active</span>
</div>
</div>
<!-- Core Details Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Username Field -->
<div class="space-y-2">
<label
class="text-[0.65rem] font-bold uppercase tracking-widest text-on-surface-variant ml-1">Username</label>
<div
class="flex items-center h-12 px-4 bg-surface-container-highest rounded-lg border border-transparent focus-within:border-primary/40 focus-within:shadow-[0_0_0_2px_rgba(55,85,195,0.05)] transition-all">
<span class="material-symbols-outlined text-on-surface-variant mr-3 text-sm"
data-icon="alternate_email">alternate_email</span>
<span class="text-sm font-medium text-on-surface">admin.aws_prod</span>
<button class="ml-auto text-on-surface-variant hover:text-primary transition-colors">
<span class="material-symbols-outlined text-base"
data-icon="content_copy">content_copy</span>
</button>
</div>
</div>
<!-- Password Field -->
<div class="space-y-2">
<label
class="text-[0.65rem] font-bold uppercase tracking-widest text-on-surface-variant ml-1">Password</label>
<div
class="flex items-center h-12 px-4 bg-surface-container-highest rounded-lg border border-transparent focus-within:border-primary/40 focus-within:shadow-[0_0_0_2px_rgba(55,85,195,0.05)] transition-all">
<span class="material-symbols-outlined text-on-surface-variant mr-3 text-sm"
data-icon="lock">lock</span>
<span class="text-sm font-medium text-on-surface">•••••••••••••••</span>
<button class="ml-auto text-on-surface-variant hover:text-primary transition-colors">
<span class="material-symbols-outlined text-base"
data-icon="visibility">visibility</span>
</button>
</div>
</div>
</div>
<!-- Metadata Row (Asymmetric Bento) -->
<div class="grid grid-cols-12 gap-4">
<!-- Owner Card -->
<div class="col-span-7 bg-surface-container-low/50 p-4 rounded-xl flex items-center gap-4">
<img alt="Alex Rivera Profile" class="w-10 h-10 rounded-full object-cover ring-2 ring-white"
data-alt="Close-up professional portrait of a male systems administrator with a friendly expression in a modern office environment"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuBi4gNrkG6OjxYer2iM7vtnmB1_dhArLqll8N46GWZ4YDXLfnwRIIf_bLhZRcMjHCxtKLivBh_JJMTnGRO4kIj0ZCtbVZ61SFhSJvZlPE3ZgNmNCCh7bDXDeFgdWnHKhWAcjDcpLmO02gp5HCU_6GJpLNdIU3pJosKGJsVW_hAhIfp8OYJcepHHf_23k3eQ9ZxkOP4ZR4qu2PU6ZmO2qTCVlJCZVtB-x6RC3YsjcpMNwpyIhSNCIcAvRKTOfU_cb2vtO6t9oD38b6o" />
<div>
<div class="text-[0.6rem] font-bold uppercase text-on-surface-variant mb-0.5">Account Owner
</div>
<div class="text-sm font-semibold text-on-surface">Alex Rivera</div>
</div>
</div>
<!-- Date Card -->
<div class="col-span-5 bg-surface-container-low/50 p-4 rounded-xl">
<div class="text-[0.6rem] font-bold uppercase text-on-surface-variant mb-0.5">Date Created</div>
<div class="text-sm font-semibold text-on-surface flex items-center gap-2">
<span class="material-symbols-outlined text-base text-on-surface-variant"
data-icon="calendar_today">calendar_today</span>
Oct 24, 2023
</div>
</div>
</div>
</div>
<!-- Footer Actions -->
<div class="px-8 py-6 bg-surface-container-low flex items-center justify-end gap-3">
<button
class="px-5 h-11 text-sm font-bold text-on-secondary-container hover:bg-surface-container transition-all rounded-lg">
Close
</button>
<button
class="px-6 h-11 text-sm font-bold text-on-primary bg-gradient-to-br from-primary to-primary-dim rounded-lg shadow-sm hover:opacity-90 active:scale-[0.98] transition-all flex items-center gap-2">
<span class="material-symbols-outlined text-base" data-icon="edit">edit</span>
Edit Account
</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -5,8 +5,8 @@
# SQL Server Connection Info # SQL Server Connection Info
$ServerName = "172.20.235.176" $ServerName = "172.20.235.176"
$Username = "sa" $Username = "sa"
$Password = "robotics@2020" $Password = "robotics@2022"
$SqlScriptPath = ".\setup.sql" $SqlScriptPath = Join-Path $PSScriptRoot "setup.sql"
Write-Host "========================================" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan
Write-Host "AccManager Database Setup" -ForegroundColor Cyan Write-Host "AccManager Database Setup" -ForegroundColor Cyan

View File

@@ -14,8 +14,10 @@ ELSE
BEGIN BEGIN
PRINT 'Database AccManager already exists.'; PRINT 'Database AccManager already exists.';
END END
GO
USE AccManager; USE AccManager;
GO
-- =========================================== -- ===========================================
-- 1. CREATE USERS TABLE -- 1. CREATE USERS TABLE

View File

@@ -0,0 +1 @@
Sqlcmd: Error: Microsoft ODBC Driver 17 for SQL Server : Login failed for user 'sa'..

623
js/app.js
View File

@@ -11,44 +11,85 @@ class AccountManager {
} }
this.currentUser = currentUser; this.currentUser = currentUser;
this.accounts = this.loadFromStorage('accounts') || []; this.accounts = [];
this.applications = this.loadFromStorage('applications') || [ this.applications = [];
{ id: 1, name: 'AWS', type: 'Cloud', status: 'online', icon: 'cloud' }, this.apiBase = '/api';
{ id: 2, name: 'GitHub', type: 'VCS', status: 'online', icon: 'code' },
{ id: 3, name: 'Google Workspace', type: 'Collaboration', status: 'online', icon: 'mail' },
{ id: 4, name: 'Nginx Proxy', type: 'Infra', status: 'offline', icon: 'dns' },
];
this.currentPage = 'dashboard'; this.currentPage = 'dashboard';
this.init(); this.initPromise = this.init();
} }
init() { 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;
}
async init() {
await this.fetchApplications();
await this.fetchAccounts();
this.setupEventListeners(); this.setupEventListeners();
// Render dashboard content this.loadModals(); // Load modals từ file riêng
// Render dashboard content (only for index.html)
const mainContent = document.getElementById('mainContent'); const mainContent = document.getElementById('mainContent');
if (mainContent) { if (mainContent && window.location.pathname.endsWith('index.html')) {
mainContent.innerHTML = this.renderDashboard(); mainContent.innerHTML = this.renderDashboard();
} }
} }
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() {
const userId = this.getUserId();
if (!userId) return;
const res = await fetch(`${this.apiBase}/accounts/user/${userId}`);
const data = await res.json();
if (data.success) {
this.accounts = data.data;
} else {
console.error('Load accounts failed:', data.message);
}
}
async loadModals() {
try {
const response = await fetch('../modals.html');
const modalsHTML = await response.text();
const modalsContainer = document.getElementById('modalsContainer');
if (modalsContainer) {
modalsContainer.innerHTML = modalsHTML;
// Re-attach event listeners after modals are loaded
setTimeout(() => {
this.setupAccountRowListeners();
this.setupFormListeners();
}, 50);
}
} catch (error) {
console.error('Lỗi load modals:', error);
}
}
setupEventListeners() { setupEventListeners() {
// Account modal
const addAccountBtn = document.getElementById('addAccountBtn');
if (addAccountBtn) {
addAccountBtn.addEventListener('click', () => this.openAccountModal());
}
// Application modal
const addAppBtn = document.getElementById('addAppBtn');
if (addAppBtn) {
addAppBtn.addEventListener('click', () => this.openAppModal());
}
// Modal close buttons // Modal close buttons
document.querySelectorAll('[data-close-modal]').forEach(btn => { document.querySelectorAll('[data-close-modal]').forEach(btn => {
btn.addEventListener('click', () => this.closeModals()); btn.addEventListener('click', () => this.closeModals());
}); });
// Close with Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.closeModals();
}
});
// Form submissions // Form submissions
const accountForm = document.getElementById('accountForm'); const accountForm = document.getElementById('accountForm');
if (accountForm) { if (accountForm) {
@@ -73,13 +114,34 @@ class AccountManager {
this.setupAccountRowListeners(); this.setupAccountRowListeners();
} }
setupFormListeners() {
const accountForm = document.getElementById('accountForm');
if (accountForm) {
accountForm.addEventListener('submit', (e) => this.handleAccountSubmit(e));
}
const appForm = document.getElementById('appForm');
if (appForm) {
appForm.addEventListener('submit', (e) => this.handleAppSubmit(e));
}
// Close when clicking backdrop outside modal content
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
backdrop.addEventListener('click', (evt) => {
if (evt.target === backdrop) {
this.closeModals();
}
});
});
}
updateAccountDisplay() { updateAccountDisplay() {
// Use the logged-in user from constructor // Use the logged-in user from constructor
const usernameEl = document.getElementById('accountUsername'); const usernameEl = document.getElementById('accountUsername');
const roleEl = document.getElementById('accountRole'); const roleEl = document.getElementById('accountRole');
if (usernameEl) usernameEl.textContent = this.currentUser?.username || 'User'; if (usernameEl) usernameEl.textContent = this.currentUser?.username || this.currentUser?.Username || 'User';
if (roleEl) roleEl.textContent = this.currentUser?.role || 'Administrator'; if (roleEl) roleEl.textContent = this.currentUser?.role || this.currentUser?.Role || 'Administrator';
} }
handleLogout() { handleLogout() {
@@ -113,7 +175,7 @@ class AccountManager {
<span class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider mb-2">Applications</span> <span class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider mb-2">Applications</span>
<div class="flex items-baseline justify-between"> <div class="flex items-baseline justify-between">
<span class="text-2xl font-black text-on-surface">${this.applications.length}</span> <span class="text-2xl font-black text-on-surface">${this.applications.length}</span>
<span class="text-[10px] font-bold text-on-surface-variant">${this.applications.filter(a => a.status === 'online').length} Active</span> <span class="text-[10px] font-bold text-on-surface-variant">${this.applications.filter(a => (a.Status || a.status) === 'online').length} Active</span>
</div> </div>
</div> </div>
<div class="bg-surface-container-lowest p-4 rounded-xl border border-outline-variant/15 flex flex-col"> <div class="bg-surface-container-lowest p-4 rounded-xl border border-outline-variant/15 flex flex-col">
@@ -148,14 +210,18 @@ class AccountManager {
</div> </div>
${this.accounts.length > 0 ? ` ${this.accounts.length > 0 ? `
<div class="flex-1 overflow-y-auto space-y-2"> <div class="flex-1 overflow-y-auto space-y-2">
${this.accounts.slice(-5).reverse().map(acc => ` ${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 `
<div class="flex gap-3 p-3 bg-surface-container-low/50 rounded-lg border-l-2 border-primary/50"> <div class="flex gap-3 p-3 bg-surface-container-low/50 rounded-lg border-l-2 border-primary/50">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="text-[11px] font-bold text-on-surface truncate">${acc.username}</p> <p class="text-[11px] font-bold text-on-surface truncate">${username}</p>
<p class="text-[9px] text-on-surface-variant">${acc.service}${acc.owner}</p> <p class="text-[9px] text-on-surface-variant">${service}${owner}</p>
</div> </div>
</div> </div>
`).join('')} `;}).join('')}
</div> </div>
` : ` ` : `
<div class="flex-1 flex items-center justify-center text-center"> <div class="flex-1 flex items-center justify-center text-center">
@@ -188,7 +254,7 @@ class AccountManager {
<span class="text-[10px] font-bold uppercase text-on-surface-variant">Service</span> <span class="text-[10px] font-bold uppercase text-on-surface-variant">Service</span>
<select id="serviceFilter" class="bg-surface-container-low border-slate-200 rounded-md text-[11px] py-1 px-2 pr-6 focus:ring-1 focus:ring-primary shadow-sm"> <select id="serviceFilter" class="bg-surface-container-low border-slate-200 rounded-md text-[11px] py-1 px-2 pr-6 focus:ring-1 focus:ring-primary shadow-sm">
<option value="">All Services</option> <option value="">All Services</option>
${this.applications.map(app => `<option value="${app.name}">${app.name}</option>`).join('')} ${this.applications.map(app => `<option value="${app.AppId}">${app.Name}</option>`).join('')}
</select> </select>
</div> </div>
<div class="flex-1"></div> <div class="flex-1"></div>
@@ -208,18 +274,21 @@ class AccountManager {
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-100"> <tbody class="divide-y divide-slate-100">
${this.accounts.map((acc, idx) => ` ${this.accounts.map(acc => `
<tr class="hover:bg-slate-50/80 transition-colors group account-row" data-account-id="${idx}"> <tr class="hover:bg-slate-50/80 transition-colors group account-row" data-account-id="${acc.AccountId}">
<td class="px-4 py-3 text-sm font-medium text-slate-900">${acc.owner}</td> <td class="px-4 py-3 text-sm font-medium text-slate-900">${acc.Email || '-'}</td>
<td class="px-4 py-3 text-sm text-slate-600">${acc.username}</td> <td class="px-4 py-3 text-sm text-slate-600">${acc.AccountUsername || '-'}</td>
<td class="px-4 py-3 text-sm"> <td class="px-4 py-3 text-sm">
<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-semibold">${acc.service}</span> <span class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-semibold">${acc.AppName || '-'}</span>
</td> </td>
<td class="px-4 py-3 text-right"> <td class="px-4 py-3 text-right">
<button class="p-1.5 text-slate-400 hover:text-primary transition-colors edit-account" data-account-id="${idx}"> <button class="p-1.5 text-slate-400 hover:text-slate-600 transition-colors view-account" data-account-id="${acc.AccountId}" title="View Details">
<span class="material-symbols-outlined text-lg">info</span>
</button>
<button class="p-1.5 text-slate-400 hover:text-primary transition-colors edit-account" data-account-id="${acc.AccountId}">
<span class="material-symbols-outlined text-lg">edit</span> <span class="material-symbols-outlined text-lg">edit</span>
</button> </button>
<button class="p-1.5 text-slate-400 hover:text-error transition-colors delete-account" data-account-id="${idx}"> <button class="p-1.5 text-slate-400 hover:text-error transition-colors delete-account" data-account-id="${acc.AccountId}">
<span class="material-symbols-outlined text-lg">delete</span> <span class="material-symbols-outlined text-lg">delete</span>
</button> </button>
</td> </td>
@@ -242,42 +311,6 @@ class AccountManager {
</div> </div>
</div> </div>
<!-- Account Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm hidden" id="accountModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Add New Account</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" data-close-modal>
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="accountForm" class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Service</label>
<select id="accountService" class="w-full border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
<option value="">Select a service</option>
${this.applications.map(app => `<option value="${app.name}">${app.name}</option>`).join('')}
</select>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Owner Name</label>
<input type="text" id="accountOwner" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="John Doe">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Username</label>
<input type="text" id="accountUsername" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="username">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Password</label>
<input type="password" id="accountPassword" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="••••••••">
</div>
<div class="flex gap-3 pt-4">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" data-close-modal>Cancel</button>
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Save Account</button>
</div>
</form>
</div>
</div>
`; `;
} }
@@ -335,36 +368,43 @@ class AccountManager {
<tr class="bg-surface-container-low/30 border-b border-outline-variant/10"> <tr class="bg-surface-container-low/30 border-b border-outline-variant/10">
<th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">Name</th> <th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">Name</th>
<th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">Type</th> <th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">Type</th>
<th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">Description</th>
<th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">URL</th>
<th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">Status</th> <th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">Status</th>
<th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest text-right">Actions</th> <th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-outline-variant/5"> <tbody class="divide-y divide-outline-variant/5">
${this.applications.map((app, idx) => ` ${this.applications.map(app => `
<tr class="hover:bg-surface-container-low/30 transition-colors group"> <tr class="hover:bg-surface-container-low/30 transition-colors group">
<td class="px-6 py-3"> <td class="px-6 py-3">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-surface-container-highest flex items-center justify-center text-primary"> <div class="w-8 h-8 rounded bg-surface-container-highest flex items-center justify-center text-primary">
<span class="material-symbols-outlined text-sm">${app.icon}</span> <span class="material-symbols-outlined text-sm">${app.Icon || 'apps'}</span>
</div> </div>
<span class="font-bold text-sm text-on-surface">${app.name}</span> <span class="font-bold text-sm text-on-surface">${app.Name}</span>
</div> </div>
</td> </td>
<td class="px-6 py-3"> <td class="px-6 py-3">
<span class="px-2 py-0.5 rounded-full text-[9px] font-black uppercase bg-surface-container-highest text-on-surface-variant">${app.type}</span> <span class="px-2 py-0.5 rounded-full text-[9px] font-black uppercase bg-surface-container-highest text-on-surface-variant">${app.Type}</span>
</td> </td>
<td class="px-6 py-3 text-sm text-on-surface-variant max-w-xs truncate" title="${app.Description || ''}">${app.Description || '-'}</td>
<td class="px-6 py-3 text-sm text-primary max-w-xs truncate">${(app.Url || app.url) ? `<a href="${app.Url || app.url}" target="_blank" class="underline">${app.Url || app.url}</a>` : '-'}</td>
<td class="px-6 py-3"> <td class="px-6 py-3">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<div class="w-1.5 h-1.5 rounded-full ${app.status === 'online' ? 'bg-primary' : 'bg-error'} ring-2 ${app.status === 'online' ? 'ring-primary/20' : 'ring-error/20'}"></div> <div class="w-1.5 h-1.5 rounded-full ${(app.Status || app.status) === 'online' ? 'bg-primary' : 'bg-error'} ring-2 ${(app.Status || app.status) === 'online' ? 'ring-primary/20' : 'ring-error/20'}"></div>
<span class="text-xs font-medium ${app.status === 'online' ? 'text-on-primary-fixed-variant' : 'text-error'}">${app.status === 'online' ? 'Online' : 'Offline'}</span> <span class="text-xs font-medium ${(app.Status || app.status) === 'online' ? 'text-on-primary-fixed-variant' : 'text-error'}">${(app.Status || app.status) === 'online' ? 'Online' : 'Offline'}</span>
</div> </div>
</td> </td>
<td class="px-6 py-3 text-right"> <td class="px-6 py-3 text-right">
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<button class="p-1.5 text-on-surface-variant hover:text-primary transition-colors edit-app" data-app-id="${idx}"> <button class="p-1.5 text-on-surface-variant hover:text-on-surface transition-colors view-app" data-app-id="${app.AppId}" title="View Details">
<span class="material-symbols-outlined text-lg">info</span>
</button>
<button class="p-1.5 text-on-surface-variant hover:text-primary transition-colors edit-app" data-app-id="${app.AppId}">
<span class="material-symbols-outlined text-lg">edit</span> <span class="material-symbols-outlined text-lg">edit</span>
</button> </button>
<button class="p-1.5 text-on-surface-variant hover:text-error transition-colors delete-app" data-app-id="${idx}"> <button class="p-1.5 text-on-surface-variant hover:text-error transition-colors delete-app" data-app-id="${app.AppId}">
<span class="material-symbols-outlined text-lg">delete</span> <span class="material-symbols-outlined text-lg">delete</span>
</button> </button>
</div> </div>
@@ -377,86 +417,228 @@ class AccountManager {
</div> </div>
</div> </div>
<!-- App Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm hidden" id="appModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Add New Application</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" data-close-modal>
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="appForm" class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Application Name</label>
<input type="text" id="appName" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="AWS Services">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Type</label>
<input type="text" id="appType" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Cloud">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Status</label>
<select id="appStatus" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
<option value="online">Online</option>
<option value="offline">Offline</option>
</select>
</div>
<div class="flex gap-3 pt-4">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" data-close-modal>Cancel</button>
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Save Application</button>
</div>
</form>
</div>
</div>
`; `;
} }
setupAccountRowListeners() { setupAccountRowListeners() {
// View Account listeners
document.querySelectorAll('.view-account').forEach(btn => {
btn.addEventListener('click', (e) => {
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 || '-';
document.getElementById('viewAccountPassword').textContent = '••••••••';
document.getElementById('viewAccountPassword').dataset.visible = 'false';
document.getElementById('toggleIcon').textContent = 'visibility';
document.getElementById('viewAccountModal').classList.add('open');
});
});
// Delete Account listeners - show confirmation modal
document.querySelectorAll('.delete-account').forEach(btn => { document.querySelectorAll('.delete-account').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
const accountId = btn.dataset.accountId; const accountId = Number(btn.dataset.accountId);
if (confirm('Delete this account?')) { const account = this.accounts.find(a => a.AccountId === accountId);
this.accounts.splice(accountId, 1); this.pendingDeleteAccountId = accountId;
this.saveToStorage('accounts', this.accounts); 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) {
alert('Account deleted successfully');
this.closeModals();
location.href = './accounts.html'; location.href = './accounts.html';
} else {
alert(data.message || 'Delete account failed');
}
})
.catch(err => {
console.error(err);
alert('Delete account failed');
});
} }
}); });
}); });
// Edit Account listeners
document.querySelectorAll('.edit-account').forEach(btn => { document.querySelectorAll('.edit-account').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
const accountId = btn.dataset.accountId; const accountId = Number(btn.dataset.accountId);
const account = this.accounts[accountId]; const account = this.accounts.find(a => a.AccountId === accountId);
// Populate form with existing data // Populate form with existing data
document.getElementById('accountUsername').value = account.username; const form = document.getElementById('accountForm');
document.getElementById('accountPassword').value = account.password; if (form) {
document.getElementById('accountOwner').value = account.owner; const userInput = form.querySelector('#accountUsername');
document.getElementById('accountService').value = account.service; const passInput = form.querySelector('#accountPassword');
this.editingAccountId = accountId; 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.editingAccountId = account?.AccountId;
this.closeModals();
this.openAccountModal(); this.openAccountModal();
}); });
}); });
document.querySelectorAll('.delete-app').forEach(btn => { // Edit from View modal
btn.addEventListener('click', (e) => { document.querySelectorAll('.edit-account-from-view').forEach(btn => {
const appId = btn.dataset.appId; btn.addEventListener('click', () => {
if (confirm('Delete this application?')) { const account = this.currentViewAccount;
this.applications.splice(appId, 1); const form = document.getElementById('accountForm');
this.saveToStorage('applications', this.applications); if (form) {
location.href = './applications.html'; 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.editingAccountId = account?.AccountId;
this.closeModals();
this.openAccountModal();
});
});
// Toggle Password Visibility
document.querySelectorAll('.toggle-password').forEach(btn => {
btn.addEventListener('click', () => {
const passwordEl = document.getElementById('viewAccountPassword');
const toggleIcon = document.getElementById('toggleIcon');
const isVisible = passwordEl.dataset.visible === 'true';
if (isVisible) {
passwordEl.textContent = '••••••••';
passwordEl.dataset.visible = 'false';
toggleIcon.textContent = 'visibility';
} else {
passwordEl.textContent = this.currentViewAccount?.AccountPassword || '';
passwordEl.dataset.visible = 'true';
toggleIcon.textContent = 'visibility_off';
} }
}); });
}); });
// 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 || '-';
document.getElementById('viewAppDescription').textContent = app?.Description || '-';
const urlEl = document.getElementById('viewAppUrl');
const urlVal = app?.Url || app?.url;
if (urlEl) {
if (urlVal) {
urlEl.innerHTML = `<a href="${urlVal}" target="_blank" class="text-primary underline">${urlVal}</a>`;
} 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) {
alert('Application deleted successfully');
this.closeModals();
location.href = './applications.html';
} else {
alert(data.message || 'Delete application failed');
}
})
.catch(err => {
console.error(err);
alert('Delete application failed');
});
}
});
});
// Edit App listeners
document.querySelectorAll('.edit-app').forEach(btn => { document.querySelectorAll('.edit-app').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
const appId = btn.dataset.appId; const appId = Number(btn.dataset.appId);
const app = this.applications[appId]; const app = this.applications.find(a => a.AppId === appId);
document.getElementById('appName').value = app.name; document.getElementById('appName').value = app?.Name || '';
document.getElementById('appType').value = app.type; document.getElementById('appType').value = app?.Type || '';
document.getElementById('appStatus').value = app.status; document.getElementById('appStatus').value = app?.Status || 'online';
this.editingAppId = appId; document.getElementById('appDescription').value = app?.Description || '';
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('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.openAccountModal();
});
});
// Add Application button
document.querySelectorAll('#addAppBtn').forEach(btn => {
btn.addEventListener('click', () => {
this.editingAppId = undefined;
this.openAppModal(); this.openAppModal();
}); });
}); });
@@ -464,67 +646,139 @@ class AccountManager {
handleAccountSubmit(e) { handleAccountSubmit(e) {
e.preventDefault(); e.preventDefault();
const newAccount = { const accountForm = document.getElementById('accountForm');
service: document.getElementById('accountService').value, const userId = this.getUserId();
owner: document.getElementById('accountOwner').value, const appId = Number(accountForm?.querySelector('#accountService')?.value || 0);
username: document.getElementById('accountUsername').value, const accountUsername = (accountForm?.querySelector('#accountUsername')?.value || '').trim();
password: document.getElementById('accountPassword').value, const accountPassword = (accountForm?.querySelector('#accountPassword')?.value || '').trim();
dateCreated: new Date().toLocaleDateString() const accountEmail = ((accountForm?.querySelector('#accountOwner')?.value || '').trim()) || this.currentUser?.Username || this.currentUser?.username || '';
if (!accountForm) {
alert('Account form not found.');
return;
}
if (!userId) {
alert('User is not authenticated. Please login again.');
return;
}
if (!appId) {
alert('Please select a service.');
return;
}
if (!accountUsername) {
alert('Please enter a username.');
return;
}
if (!accountPassword) {
alert('Please enter a password.');
return;
}
const payload = {
userId,
appId,
accountUsername,
accountPassword,
email: accountEmail,
accessLevel: 'user',
notes: ''
}; };
if (this.editingAccountId !== undefined) { const isEdit = this.editingAccountId !== undefined;
this.accounts[this.editingAccountId] = newAccount; const url = isEdit ? `${this.apiBase}/accounts/${this.editingAccountId}` : `${this.apiBase}/accounts`;
this.editingAccountId = undefined; const method = isEdit ? 'PUT' : 'POST';
} else {
this.accounts.push(newAccount);
}
this.saveToStorage('accounts', this.accounts); fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(res => res.json()).then(data => {
if (data.success) {
this.editingAccountId = undefined;
alert(isEdit ? 'Account updated successfully' : 'Account created successfully');
this.closeModals(); this.closeModals();
location.href = './accounts.html'; location.href = './accounts.html';
} else {
alert(data.message || 'Save account failed');
}
}).catch(err => {
console.error(err);
alert('Save account failed');
});
} }
handleAppSubmit(e) { handleAppSubmit(e) {
e.preventDefault(); e.preventDefault();
const newApp = { const payload = {
name: document.getElementById('appName').value, name: document.getElementById('appName').value,
type: document.getElementById('appType').value, type: document.getElementById('appType').value,
status: document.getElementById('appStatus').value, status: document.getElementById('appStatus').value,
icon: 'cloud' icon: 'cloud',
description: document.getElementById('appDescription')?.value || '',
url: (document.getElementById('appUrl')?.value || '').trim()
}; };
if (this.editingAppId !== undefined) { const isEdit = this.editingAppId !== undefined;
this.applications[this.editingAppId] = newApp; const url = isEdit ? `${this.apiBase}/applications/${this.editingAppId}` : `${this.apiBase}/applications`;
this.editingAppId = undefined; const method = isEdit ? 'PUT' : 'POST';
} else {
this.applications.push(newApp);
}
this.saveToStorage('applications', this.applications); fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(res => res.json()).then(data => {
if (data.success) {
this.editingAppId = undefined;
alert(isEdit ? 'Application updated successfully' : 'Application created successfully');
this.closeModals(); this.closeModals();
location.href = './applications.html'; location.href = './applications.html';
} else {
alert(data.message || 'Save application failed');
}
}).catch(err => {
console.error(err);
alert('Save application failed');
});
} }
openAccountModal() { openAccountModal() {
this.editingAccountId = undefined; // Refresh service options so newly added applications appear
document.getElementById('accountService').value = ''; const serviceSelect = document.getElementById('accountService');
document.getElementById('accountOwner').value = ''; if (serviceSelect) {
document.getElementById('accountUsername').value = ''; serviceSelect.innerHTML = `<option value="">Select a service</option>` +
document.getElementById('accountPassword').value = ''; this.applications.map(app => `<option value="${app.AppId}">${app.Name}</option>`).join('');
document.getElementById('accountModal').classList.remove('hidden'); }
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() { openAppModal() {
this.editingAppId = undefined; if (this.editingAppId === undefined) {
document.getElementById('appName').value = ''; document.getElementById('appName').value = '';
document.getElementById('appType').value = ''; document.getElementById('appType').value = '';
document.getElementById('appStatus').value = 'online'; document.getElementById('appStatus').value = 'online';
document.getElementById('appModal').classList.remove('hidden'); const desc = document.getElementById('appDescription');
const url = document.getElementById('appUrl');
if (desc) desc.value = '';
if (url) url.value = '';
}
document.getElementById('appModal').classList.add('open');
} }
closeModals() { closeModals() {
document.querySelectorAll('.modal-backdrop').forEach(modal => { document.querySelectorAll('.modal-backdrop').forEach(modal => {
modal.classList.add('hidden'); modal.classList.remove('open');
}); });
} }
@@ -538,6 +792,37 @@ class AccountManager {
} }
} }
// 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');
}
// Initialize app when DOM is ready // Initialize app when DOM is ready
let app; let app;
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {

187
modals.html Normal file
View File

@@ -0,0 +1,187 @@
<!-- Add Account Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="accountModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Add New Account</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAccountModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="accountForm" class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Service</label>
<select id="accountService" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
<option value="">Select a service</option>
</select>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Owner Name</label>
<input type="text" id="accountOwner" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" required placeholder="Current user" readonly>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Username</label>
<input type="text" id="accountUsername" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="username">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Password</label>
<input type="password" id="accountPassword" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="••••••••">
</div>
<div class="flex gap-3 pt-4">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeAccountModal()">Cancel</button>
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Save Account</button>
</div>
</form>
</div>
</div>
<!-- View Account Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="viewAccountModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Account Details</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeViewAccountModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<div class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Service</label>
<div id="viewAccountService" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Owner Name</label>
<div id="viewAccountOwner" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Username</label>
<div id="viewAccountUsername" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Password</label>
<div class="flex items-center gap-2">
<div id="viewAccountPassword" class="flex-1 border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">••••••••</div>
<button type="button" class="p-2 rounded-lg hover:bg-slate-100 text-slate-400 transition-colors toggle-password">
<span class="material-symbols-outlined text-lg" id="toggleIcon">visibility</span>
</button>
</div>
</div>
<div class="flex gap-3 pt-4">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeViewAccountModal()">Close</button>
<button type="button" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold edit-account-from-view">Edit</button>
</div>
</div>
</div>
</div>
<!-- Delete Account Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteAccountModal">
<div class="modal-content w-full max-w-md bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 bg-red-50 flex items-center gap-3">
<span class="material-symbols-outlined text-red-600 text-2xl">warning</span>
<h3 class="text-base font-extrabold text-red-700">Delete Account</h3>
</div>
<div class="p-6">
<p class="text-sm text-slate-600 mb-6">Are you sure you want to delete the account for <strong id="deleteAccountUsername">-</strong>? This action cannot be undone.</p>
<div class="flex gap-3">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeDeleteAccountModal()">Cancel</button>
<button type="button" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold confirm-delete-account">Delete</button>
</div>
</div>
</div>
</div>
<!-- Add App Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="appModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Add New Application</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAppModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="appForm" class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Application Name</label>
<input type="text" id="appName" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="AWS Services">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Type</label>
<input type="text" id="appType" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Cloud">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Description</label>
<textarea id="appDescription" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none" placeholder="Short description"></textarea>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">URL</label>
<input type="text" id="appUrl" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="https://example.com or 172.20.235.176">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Status</label>
<select id="appStatus" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
<option value="online">Online</option>
<option value="offline">Offline</option>
</select>
</div>
<div class="flex gap-3 pt-4">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeAppModal()">Cancel</button>
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Save Application</button>
</div>
</form>
</div>
</div>
<!-- View App Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="viewAppModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Application Details</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeViewAppModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<div class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Application Name</label>
<div id="viewAppName" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Type</label>
<div id="viewAppType" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Description</label>
<div id="viewAppDescription" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600 break-words">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">URL</label>
<div id="viewAppUrl" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600 break-all">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Status</label>
<div id="viewAppStatus" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div class="flex gap-3 pt-4">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeViewAppModal()">Close</button>
<button type="button" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold edit-app-from-view">Edit</button>
</div>
</div>
</div>
</div>
<!-- Delete App Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteAppModal">
<div class="modal-content w-full max-w-md bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 bg-red-50 flex items-center gap-3">
<span class="material-symbols-outlined text-red-600 text-2xl">warning</span>
<h3 class="text-base font-extrabold text-red-700">Delete Application</h3>
</div>
<div class="p-6">
<p class="text-sm text-slate-600 mb-6">Are you sure you want to delete <strong id="deleteAppName">-</strong>? This action cannot be undone.</p>
<div class="flex gap-3">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeDeleteAppModal()">Cancel</button>
<button type="button" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold confirm-delete-app">Delete</button>
</div>
</div>
</div>
</div>

View File

@@ -135,15 +135,111 @@
</div> </div>
</header> </header>
<div id="mainContent" class="flex-1 overflow-hidden"></div> <div id="mainContent" class="flex-1 overflow-hidden"></div>
<div id="modalsContainer">
<!-- Add Account Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="accountModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Add New Account</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAccountModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="accountForm" class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Service</label>
<select id="accountService" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
<option value="">Select a service</option>
</select>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Owner Name</label>
<input type="text" id="accountOwner" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="John Doe">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Username</label>
<input type="text" id="accountUsername" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="username">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Password</label>
<input type="password" id="accountPassword" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="••••••••">
</div>
<div class="flex gap-3 pt-4">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeAccountModal()">Cancel</button>
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Save Account</button>
</div>
</form>
</div>
</div>
<!-- View Account Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="viewAccountModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Account Details</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeViewAccountModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<div class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Service</label>
<div id="viewAccountService" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Owner Name</label>
<div id="viewAccountOwner" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Username</label>
<div id="viewAccountUsername" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Password</label>
<div class="flex items-center gap-2">
<div id="viewAccountPassword" class="flex-1 border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">••••••••</div>
<button type="button" class="p-2 rounded-lg hover:bg-slate-100 text-slate-400 transition-colors toggle-password">
<span class="material-symbols-outlined text-lg" id="toggleIcon">visibility</span>
</button>
</div>
</div>
<div class="flex gap-3 pt-4">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeViewAccountModal()">Close</button>
<button type="button" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold edit-account-from-view">Edit</button>
</div>
</div>
</div>
</div>
<!-- Delete Account Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteAccountModal">
<div class="modal-content w-full max-w-md bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 bg-red-50 flex items-center gap-3">
<span class="material-symbols-outlined text-red-600 text-2xl">warning</span>
<h3 class="text-base font-extrabold text-red-700">Delete Account</h3>
</div>
<div class="p-6">
<p class="text-sm text-slate-600 mb-6">Are you sure you want to delete the account for <strong id="deleteAccountUsername">-</strong>? This action cannot be undone.</p>
<div class="flex gap-3">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeDeleteAccountModal()">Cancel</button>
<button type="button" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold confirm-delete-account">Delete</button>
</div>
</div>
</div>
</div>
</div>
</main> </main>
<script src="../js/app.js"></script> <script src="../js/app.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', async () => {
if (app) { if (app) {
const mainContent = document.getElementById('mainContent'); const mainContent = document.getElementById('mainContent');
const modalsContainer = document.getElementById('modalsContainer');
await app.initPromise;
if (mainContent) { if (mainContent) {
mainContent.innerHTML = app.getAccountsContent(); mainContent.innerHTML = app.getAccountsContent();
app.setupAccountRowListeners(); app.setupAccountRowListeners();
app.setupAddButtonListeners();
} }
} }
}); });

View File

@@ -135,15 +135,98 @@
</div> </div>
</header> </header>
<div id="mainContent" class="flex-1 overflow-hidden"></div> <div id="mainContent" class="flex-1 overflow-hidden"></div>
<div id="modalsContainer">
<!-- Add App Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="appModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Add New Application</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAppModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="appForm" class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Application Name</label>
<input type="text" id="appName" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="AWS Services">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Type</label>
<input type="text" id="appType" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Cloud">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Status</label>
<select id="appStatus" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
<option value="online">Online</option>
<option value="offline">Offline</option>
</select>
</div>
<div class="flex gap-3 pt-4">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeAppModal()">Cancel</button>
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Save Application</button>
</div>
</form>
</div>
</div>
<!-- View App Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="viewAppModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Application Details</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeViewAppModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<div class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Application Name</label>
<div id="viewAppName" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Type</label>
<div id="viewAppType" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Status</label>
<div id="viewAppStatus" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">-</div>
</div>
<div class="flex gap-3 pt-4">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeViewAppModal()">Close</button>
<button type="button" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold edit-app-from-view">Edit</button>
</div>
</div>
</div>
</div>
<!-- Delete App Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteAppModal">
<div class="modal-content w-full max-w-md bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 bg-red-50 flex items-center gap-3">
<span class="material-symbols-outlined text-red-600 text-2xl">warning</span>
<h3 class="text-base font-extrabold text-red-700">Delete Application</h3>
</div>
<div class="p-6">
<p class="text-sm text-slate-600 mb-6">Are you sure you want to delete <strong id="deleteAppName">-</strong>? This action cannot be undone.</p>
<div class="flex gap-3">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeDeleteAppModal()">Cancel</button>
<button type="button" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold confirm-delete-app">Delete</button>
</div>
</div>
</div>
</div>
</div>
</main> </main>
<script src="../js/app.js"></script> <script src="../js/app.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', async () => {
if (app) { if (app) {
const mainContent = document.getElementById('mainContent'); const mainContent = document.getElementById('mainContent');
await app.initPromise;
if (mainContent) { if (mainContent) {
mainContent.innerHTML = app.getApplicationsContent(); mainContent.innerHTML = app.getApplicationsContent();
app.setupAccountRowListeners(); app.setupAccountRowListeners();
app.setupAddButtonListeners();
} }
} }
}); });

136
server.js
View File

@@ -10,6 +10,17 @@ const app = express();
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
// Serve static files
const path = require('path');
app.use(express.static(path.join(__dirname, 'pages')));
app.use(express.static(path.join(__dirname, 'js')));
app.use(express.static(path.join(__dirname)));
// Root route
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'pages', 'login.html'));
});
// SQL Server Configuration // SQL Server Configuration
const sqlConfig = { const sqlConfig = {
server: '172.20.235.176', server: '172.20.235.176',
@@ -17,14 +28,15 @@ const sqlConfig = {
type: 'default', type: 'default',
options: { options: {
userName: 'sa', userName: 'sa',
password: 'robotics@2020' password: 'robotics@2022'
} }
}, },
options: { options: {
database: 'AccManager', database: 'AccManager',
trustServerCertificate: true, trustServerCertificate: true,
enableKeepAlive: true, enableKeepAlive: true,
connectTimeout: 30000 connectTimeout: 30000,
encrypt: false
} }
}; };
@@ -40,8 +52,8 @@ async function initializeDatabase() {
// Check and create database if not exists // Check and create database if not exists
const masterConnection = new sql.ConnectionPool({ const masterConnection = new sql.ConnectionPool({
server: '172.20.235.176', server: '172.20.235.176',
authentication: { type: 'default', options: { userName: 'sa', password: 'robotics@2020' } }, authentication: { type: 'default', options: { userName: 'sa', password: 'robotics@2022' } },
options: { connectTimeout: 30000, database: 'master', trustServerCertificate: true } options: { connectTimeout: 30000, database: 'master', trustServerCertificate: true, encrypt: false }
}); });
await masterConnection.connect(); await masterConnection.connect();
@@ -91,6 +103,7 @@ async function createTables() {
Status NVARCHAR(20) DEFAULT 'online', Status NVARCHAR(20) DEFAULT 'online',
Icon NVARCHAR(50), Icon NVARCHAR(50),
Description NVARCHAR(500), Description NVARCHAR(500),
Url NVARCHAR(255),
CreatedDate DATETIME DEFAULT GETDATE(), CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE() UpdatedDate DATETIME DEFAULT GETDATE()
) )
@@ -141,6 +154,16 @@ async function createTables() {
} }
} }
// Ensure new columns exist on Applications for migrations
try {
await pool.request().query(`IF COL_LENGTH('dbo.Applications','Url') IS NULL ALTER TABLE Applications ADD Url NVARCHAR(255);`);
await pool.request().query(`IF COL_LENGTH('dbo.Applications','Description') IS NULL ALTER TABLE Applications ADD Description NVARCHAR(500);`);
// Backfill Url to empty string to avoid undefined in responses
await pool.request().query(`UPDATE Applications SET Url = '' WHERE Url IS NULL;`);
} catch (err) {
console.error('Column addition error (Applications):', err.message);
}
// Insert initial admin user // Insert initial admin user
try { try {
await pool.request() await pool.request()
@@ -162,12 +185,12 @@ async function createTables() {
await pool.request() await pool.request()
.query(`IF (SELECT COUNT(*) FROM Applications) = 0 .query(`IF (SELECT COUNT(*) FROM Applications) = 0
BEGIN BEGIN
INSERT INTO Applications (Name, Type, Status, Icon, Description) INSERT INTO Applications (Name, Type, Status, Icon, Description, Url)
VALUES VALUES
('AWS', 'Cloud', 'online', 'cloud', 'Amazon Web Services'), ('AWS', 'Cloud', 'online', 'cloud', 'Amazon Web Services', 'https://aws.amazon.com'),
('GitHub', 'VCS', 'online', 'code', 'GitHub - Version Control'), ('GitHub', 'VCS', 'online', 'code', 'GitHub - Version Control', 'https://github.com'),
('Google Workspace', 'Collaboration', 'online', 'mail', 'Google Workspace'), ('Google Workspace', 'Collaboration', 'online', 'mail', 'Google Workspace', 'https://workspace.google.com'),
('Nginx Proxy', 'Infra', 'offline', 'dns', 'Nginx Web Server') ('Nginx Proxy', 'Infra', 'offline', 'dns', 'Nginx Web Server', 'https://nginx.org')
END`); END`);
console.log('✓ Sample applications created'); console.log('✓ Sample applications created');
} catch (err) { } catch (err) {
@@ -275,7 +298,7 @@ app.post('/api/users', async (req, res) => {
app.get('/api/applications', async (req, res) => { app.get('/api/applications', async (req, res) => {
try { try {
const result = await pool.request() const result = await pool.request()
.query('SELECT * FROM Applications ORDER BY Name'); .query('SELECT AppId, Name, Type, Status, Icon, Description, Url, CreatedDate, UpdatedDate FROM Applications ORDER BY Name');
res.json({ success: true, data: result.recordset }); res.json({ success: true, data: result.recordset });
} catch (err) { } catch (err) {
res.status(500).json({ success: false, message: err.message }); res.status(500).json({ success: false, message: err.message });
@@ -285,7 +308,7 @@ app.get('/api/applications', async (req, res) => {
// Create application // Create application
app.post('/api/applications', async (req, res) => { app.post('/api/applications', async (req, res) => {
try { try {
const { name, type, status, icon, description } = req.body; const { name, type, status, icon, description, url } = req.body;
const result = await pool.request() const result = await pool.request()
.input('name', sql.NVarChar, name) .input('name', sql.NVarChar, name)
@@ -293,8 +316,9 @@ app.post('/api/applications', async (req, res) => {
.input('status', sql.NVarChar, status) .input('status', sql.NVarChar, status)
.input('icon', sql.NVarChar, icon) .input('icon', sql.NVarChar, icon)
.input('description', sql.NVarChar, description) .input('description', sql.NVarChar, description)
.query(`INSERT INTO Applications (Name, Type, Status, Icon, Description) .input('url', sql.NVarChar, url)
VALUES (@name, @type, @status, @icon, @description); .query(`INSERT INTO Applications (Name, Type, Status, Icon, Description, Url)
VALUES (@name, @type, @status, @icon, @description, @url);
SELECT SCOPE_IDENTITY() as AppId`); SELECT SCOPE_IDENTITY() as AppId`);
res.json({ success: true, message: 'Application created', appId: result.recordset[0].AppId }); res.json({ success: true, message: 'Application created', appId: result.recordset[0].AppId });
@@ -303,6 +327,48 @@ app.post('/api/applications', async (req, res) => {
} }
}); });
// Update application
app.put('/api/applications/:id', async (req, res) => {
try {
const { name, type, status, icon, description, url } = req.body;
await pool.request()
.input('appId', sql.Int, req.params.id)
.input('name', sql.NVarChar, name)
.input('type', sql.NVarChar, type)
.input('status', sql.NVarChar, status)
.input('icon', sql.NVarChar, icon)
.input('description', sql.NVarChar, description)
.input('url', sql.NVarChar, url)
.query(`UPDATE Applications
SET Name = @name,
Type = @type,
Status = @status,
Icon = @icon,
Description = @description,
Url = @url,
UpdatedDate = GETDATE()
WHERE AppId = @appId`);
res.json({ success: true, message: 'Application updated' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Delete application
app.delete('/api/applications/:id', async (req, res) => {
try {
await pool.request()
.input('appId', sql.Int, req.params.id)
.query('DELETE FROM Applications WHERE AppId = @appId');
res.json({ success: true, message: 'Application deleted' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ========================================== // ==========================================
// API ROUTES - Accounts // API ROUTES - Accounts
// ========================================== // ==========================================
@@ -346,6 +412,50 @@ app.post('/api/accounts', async (req, res) => {
} }
}); });
// Update account
app.put('/api/accounts/:id', async (req, res) => {
try {
const { userId, appId, accountUsername, accountPassword, email, accessLevel, notes } = req.body;
await pool.request()
.input('accountId', sql.Int, req.params.id)
.input('userId', sql.Int, userId)
.input('appId', sql.Int, appId)
.input('accountUsername', sql.NVarChar, accountUsername)
.input('accountPassword', sql.NVarChar, accountPassword)
.input('email', sql.NVarChar, email)
.input('accessLevel', sql.NVarChar, accessLevel)
.input('notes', sql.NVarChar, notes)
.query(`UPDATE Accounts
SET UserId = @userId,
AppId = @appId,
AccountUsername = @accountUsername,
AccountPassword = @accountPassword,
Email = @email,
AccessLevel = @accessLevel,
Notes = @notes,
UpdatedDate = GETDATE()
WHERE AccountId = @accountId`);
res.json({ success: true, message: 'Account updated' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Delete account
app.delete('/api/accounts/:id', async (req, res) => {
try {
await pool.request()
.input('accountId', sql.Int, req.params.id)
.query('DELETE FROM Accounts WHERE AccountId = @accountId');
res.json({ success: true, message: 'Account deleted' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ========================================== // ==========================================
// API ROUTES - Database Info // API ROUTES - Database Info
// ========================================== // ==========================================

BIN
setup_full_log.txt Normal file

Binary file not shown.

53
setup_output.log Normal file
View File

@@ -0,0 +1,53 @@
Database AccManager created successfully.
Changed database context to 'AccManager'.
Table Users created successfully.
Table Applications created successfully.
Table Accounts created successfully.
Table AuditLog created successfully.
Indexes created successfully.
(1 rows affected)
Admin user created: Username=admin, Password=admin
(4 rows affected)
Sample applications inserted successfully.
========================================
DATABASE SETUP COMPLETED SUCCESSFULLY
========================================
Database Name: AccManager
Tables created:
TableName
------------------------------------------------------------------------------------------------------------------------------------
- Accounts
- Applications
- AuditLog
- Users
(4 rows affected)
Users in system:
UserInfo
--------------------------------------------------------------------------------------------------------------------------------------------------------
Username: admin | Role: admin | Status: Active
(1 rows affected)
Applications available:
AppInfo
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- AWS (Cloud) - online
- GitHub (VCS) - online
- Google Workspace (Collaboration) - online
- Nginx Proxy (Infra) - offline
(4 rows affected)
Login Credentials:
Username: admin
Password: admin
Role: admin
========================================