save
This commit is contained in:
2
.env
2
.env
@@ -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
|
||||||
|
|||||||
224
codedialog.html
Normal file
224
codedialog.html
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<!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&family=Inter:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&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>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
1
database/setup_output.log
Normal file
1
database/setup_output.log
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Sqlcmd: Error: Microsoft ODBC Driver 17 for SQL Server : Login failed for user 'sa'..
|
||||||
623
js/app.js
623
js/app.js
@@ -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
187
modals.html
Normal 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>
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
136
server.js
@@ -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
BIN
setup_full_log.txt
Normal file
Binary file not shown.
53
setup_output.log
Normal file
53
setup_output.log
Normal 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
|
||||||
|
|
||||||
|
========================================
|
||||||
Reference in New Issue
Block a user