Compare commits

..

31 Commits

Author SHA1 Message Date
395b1f6e85 fixx 01 2026-05-06 16:56:58 +07:00
9f14491562 trạng thái 2026-05-06 16:36:12 +07:00
d88aa39bd6 xuất tài sản 2026-05-06 16:04:56 +07:00
8b2a9d7afe import fix 2026-05-06 11:14:10 +07:00
197186eac8 add dự án 2026-04-25 21:34:17 +07:00
8bd67200ce forgot pass 2026-04-25 11:58:09 +07:00
4fb7f412bf fix mượn trả 2026-04-25 09:24:35 +07:00
bc7a484a01 color
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 16:34:14 +07:00
d4800beb67 request 2026-04-24 16:11:00 +07:00
3961514f6c 24/04/2026 - mã tài sản 2026-04-24 13:56:12 +07:00
9526628334 test 2026-04-22 17:18:31 +07:00
6dc2391858 ok 2026-04-22 17:11:01 +07:00
bcc22b1971 ignore .env 2026-04-22 17:10:23 +07:00
61c4415ff7 push 2026-04-22 16:59:13 +07:00
1408294922 import 2026-04-22 16:57:48 +07:00
5f3e38a3d4 Update README.md 2026-04-20 08:32:13 +00:00
a8933011dd delete 2026-04-20 08:31:37 +00:00
aada849671 update day 2026-04-20 15:30:56 +07:00
3b813a85da sercurity 2026-04-20 15:27:27 +07:00
30e326f92a role 2026-04-20 13:45:29 +07:00
ee10a7e480 sercurity 2026-04-03 09:06:38 +07:00
e59785e06b readme 2026-04-02 15:45:27 +07:00
27c2a4c51e reponsive 2026-04-02 15:41:23 +07:00
a78769cfde Merge branch 'main' of https://git.pnkr.asia/DungTT/ManagerAccount 2026-04-02 15:24:03 +07:00
5a7bf191d0 confirm email 2026-04-02 15:23:32 +07:00
b3c1283694 Update DEPLOYMENT_GUIDE.md 2026-04-02 08:20:53 +00:00
6c2e89dc93 update role 2026-04-02 13:34:11 +07:00
c823549458 readme 2026-04-02 11:26:14 +07:00
d09ba3d2ad done ver1.0.0 2026-04-02 11:16:18 +07:00
58dbefa155 done manager account 2026-03-31 14:14:01 +07:00
900a569c51 fomat 2026-03-31 13:48:10 +07:00
45 changed files with 16158 additions and 4485 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
node_modules
npm-debug.log
.git
.gitignore
.vscode
.idea
.vs
.env
docker-compose.yml

27
.env
View File

@@ -1,6 +1,15 @@
# AccManager Backend Configuration # AccManager Backend Configuration
# Server Port # Application
NODE_ENV=production
# Host port exposed by docker compose
APP_PORT=3000
# Image used for server pull deployment
DOCKER_IMAGE=toiiiiday/accmanager:1.2.1
# Container app port
PORT=3000 PORT=3000
# SQL Server Configuration # SQL Server Configuration
@@ -8,8 +17,18 @@ DB_SERVER=172.20.235.176
DB_USER=sa DB_USER=sa
DB_PASSWORD=robotics@2022 DB_PASSWORD=robotics@2022
DB_NAME=AccManager DB_NAME=AccManager
DB_ENCRYPT=true DB_ENCRYPT=false
DB_TRUST_CERTIFICATE=true DB_TRUST_CERTIFICATE=true
DB_CONNECT_TIMEOUT=30000
# Application # Security
NODE_ENV=development BCRYPT_ROUNDS=12
# Email Verification (SMTP)
APP_BASE_URL=https://accrobot.pnkr.asia/
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=robotics.gitlab.2fa@gmail.com
SMTP_PASS=jaljalhreomqruqo
SMTP_FROM=robotics.gitlab.2fa@gmail.com

View File

@@ -1,291 +0,0 @@
# AccManager Backend Setup Guide
## 📋 Database Information
**Server:** 172.20.235.176
**Database:** AccManager
**User:** sa
**Password:** robotics@2020
## 📊 Database Structure
### Tables Created
#### 1. **Users** - Quản lý người dùng
- `UserId` (INT) - Primary Key
- `Username` (NVARCHAR) - Unique
- `Password` (NVARCHAR)
- `Email` (NVARCHAR)
- `FullName` (NVARCHAR)
- `Role` (NVARCHAR) - admin, user, viewer
- `Status` (NVARCHAR) - Active/Inactive
- `CreatedDate` (DATETIME)
- `LastLogin` (DATETIME)
- `IsActive` (BIT)
#### 2. **Applications** - Danh sách ứng dụng
- `AppId` (INT) - Primary Key
- `Name` (NVARCHAR)
- `Type` (NVARCHAR) - Cloud, VCS, Collaboration, Infra
- `Status` (NVARCHAR) - online/offline
- `Icon` (NVARCHAR)
- `Description` (NVARCHAR)
- `CreatedDate` (DATETIME)
- `UpdatedDate` (DATETIME)
#### 3. **Accounts** - Tài khoản ứng dụng
- `AccountId` (INT) - Primary Key
- `UserId` (INT) - Foreign Key
- `AppId` (INT) - Foreign Key
- `AccountUsername` (NVARCHAR)
- `AccountPassword` (NVARCHAR)
- `Email` (NVARCHAR)
- `AccessLevel` (NVARCHAR)
- `Status` (NVARCHAR)
- `Notes` (NVARCHAR)
- `CreatedDate` (DATETIME)
- `UpdatedDate` (DATETIME)
#### 4. **AuditLog** - Nhật ký hoạt động
- `LogId` (INT) - Primary Key
- `UserId` (INT) - Foreign Key
- `Action` (NVARCHAR) - INSERT, UPDATE, DELETE
- `TableName` (NVARCHAR)
- `RecordId` (INT)
- `OldValue` (NVARCHAR)
- `NewValue` (NVARCHAR)
- `Timestamp` (DATETIME)
## 🔐 Default Admin Account
**Username:** admin
**Password:** admin
**Role:** admin
**Status:** Active
## 🚀 Installation & Setup
### 1. Install Node.js Dependencies
```bash
npm install
```
### 2. Run Backend Server
```bash
npm start
```
Server sẽ chạy tại: **http://localhost:3000**
### 3. Kiểm tra Database Connection
```bash
curl http://localhost:3000/api/health
```
Response:
```json
{
"status": "OK",
"database": "Connected"
}
```
## 📡 API Endpoints
### Authentication
#### Login
```bash
POST /api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "admin"
}
```
### Users
#### Get All Users
```bash
GET /api/users
```
#### Get User Details
```bash
GET /api/users/:id
```
#### Create New User
```bash
POST /api/users
Content-Type: application/json
{
"username": "newuser",
"password": "password123",
"email": "user@example.com",
"fullname": "Full Name",
"role": "user"
}
```
### Applications
#### Get All Applications
```bash
GET /api/applications
```
#### Create Application
```bash
POST /api/applications
Content-Type: application/json
{
"name": "New App",
"type": "Cloud",
"status": "online",
"icon": "cloud",
"description": "Application description"
}
```
### Accounts
#### Get User Accounts
```bash
GET /api/accounts/user/:userId
```
#### Create Account
```bash
POST /api/accounts
Content-Type: application/json
{
"userId": 1,
"appId": 1,
"accountUsername": "account_user",
"accountPassword": "account_pass",
"email": "account@example.com",
"accessLevel": "Admin",
"notes": "Account notes"
}
```
### Database Info
#### Get Database Statistics
```bash
GET /api/database/info
```
Response:
```json
{
"success": true,
"database": "AccManager",
"server": "172.20.235.176",
"tables": [
{
"TableName": "Accounts",
"ColumnCount": 11
},
{
"TableName": "Applications",
"ColumnCount": 7
},
{
"TableName": "AuditLog",
"ColumnCount": 8
},
{
"TableName": "Users",
"ColumnCount": 10
}
],
"statistics": {
"users": 1,
"applications": 4,
"accounts": 0
}
}
```
## 🔧 Frontend Integration
Update frontend API calls to use the backend server:
```javascript
// Change from localStorage to API calls
const API_URL = 'http://localhost:3000/api';
// Example: Login
async function login(username, password) {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
return response.json();
}
// Example: Get user accounts
async function getAccounts(userId) {
const response = await fetch(`${API_URL}/accounts/user/${userId}`);
return response.json();
}
```
## 📝 Initial Data Created
### Users
- **admin** (admin role)
### Applications
- AWS (Cloud) - online
- GitHub (VCS) - online
- Google Workspace (Collaboration) - online
- Nginx Proxy (Infra) - offline
## 🐛 Troubleshooting
### Connection Error
```
Database connection failed: Error
```
**Solution:**
- Kiểm tra SQL Server đang chạy
- Kiểm tra network connectivity đến 172.20.235.176
- Kiểm tra username/password đúng
- Kiểm tra SQL Server Authentication được enable
### Port Already in Use
```
listen EADDRINUSE: address already in use :::3000
```
**Solution:**
```bash
# Change port in .env
PORT=3001
```
### MSSQL Module Not Found
```bash
npm install mssql
```
## 📚 References
- [ExpressJS Documentation](https://expressjs.com/)
- [MSSQL Package](https://github.com/tediousjs/node-mssql)
- [SQL Server Documentation](https://docs.microsoft.com/en-us/sql/)
---
**Status:** ✓ Database created, ✓ Tables created, ✓ Admin user created, ✓ Backend ready

296
DEPLOYMENT_GUIDE.md Normal file
View File

@@ -0,0 +1,296 @@
# 📘 Hướng dẫn Triển khai AccManager
Tài liệu này hướng dẫn chi tiết cách build Docker image, push lên registry, và triển khai trên server.
---
## 📋 Mục lục
1. [Chuẩn bị](#chuẩn-bị)
2. [Máy DEV: Build & Push Image](#máy-dev-build--push-image)
3. [Máy Server: Pull & Deploy](#máy-server-pull--deploy)
4. [Kiểm tra & Troubleshoot](#kiểm-tra--troubleshoot)
5. [Cập nhật bản mới](#cập-nhật-bản-mới)
6. [Public Domain qua Nginx Proxy Manager](#public-domain-qua-nginx-proxy-manager)
---
## 🔧 Chuẩn bị
### Tài khoản & Biến môi trường
1. **Docker Hub Account**: Tạo account tại https://hub.docker.com
- Username: `toiiiiday` (dùng username của bạn)
- Repository: `accmanager`
2. **File .env trên máy dev** - Kiểm tra nội dung:
```env
NODE_ENV=production
APP_PORT=3000
DOCKER_IMAGE=toiiiiday/accmanager:1.0.1
PORT=3000
DB_SERVER=172.20.235.176
DB_USER=sa
DB_PASSWORD=robotics@2022
DB_NAME=AccManager
DB_ENCRYPT=false
DB_TRUST_CERTIFICATE=true
DB_CONNECT_TIMEOUT=30000
BCRYPT_ROUNDS=12
```
3. **Thư mục trên server** - SSH vào server tạo:
```bash
mkdir -p ~/accmanager
cd ~/accmanager
```
---
## 🖥️ Máy DEV: Build & Push Image
### Bước 1: Chọn version mới
Mỗi lần sửa code và muốn deploy, chọn một tag mới theo thứ tự tăng dần.
Ví dụ:
- Bản cũ đang chạy: `1.0.2`
- Bản mới sau khi sửa code: `1.0.3`
### Bước 2: Build image mới (trên máy DEV)
```powershell
cd D:\RoboticsSource\AccManager
docker build -t toiiiiday/accmanager:"version" .
```
### Bước 3: Push image mới lên Docker Hub
```powershell
docker push toiiiiday/accmanager:"version"
```
### Bước 4: Kiểm tra image đã có trên registry
```powershell
docker image ls | findstr toiiiiday/accmanager
```
Hoặc kiểm tra trên Docker Hub:
https://hub.docker.com/r/toiiiiday/accmanager/tags
### Bước 5: Cập nhật .env
Sửa dòng `DOCKER_IMAGE`:
```
DOCKER_IMAGE=toiiiiday/accmanager:"version"
```
Mẹo PowerShell (cập nhật nhanh):
```powershell
(Get-Content .env) -replace '^DOCKER_IMAGE=.*', 'DOCKER_IMAGE=toiiiiday/accmanager:1.0.3' | Set-Content .env
```
### Bước 6: Copy confi lên server
Từ máy dev:
```powershell
scp .env robotics@172.20.235.176:~/accmanager/.env
scp docker-compose.yml robotics@172.20.235.176:~/accmanager/docker-compose.yml
scp docker-compose.image.yml robotics@172.20.235.176:~/accmanager/docker-compose.image.yml
```
---
## 🐧 Máy Server: Pull & Deploy
### Bước 1: SSH vào server
```bash
ssh robotics@172.20.235.176
```
### Bước 2: Vào thư mục deploy
```bash
cd ~/accmanager
```
### Bước 3: Pull image mới và chạy lại container
```bash
docker compose --env-file .env -f docker-compose.image.yml pull accmanager
docker compose --env-file .env -f docker-compose.image.yml up -d accmanager
```
Kiểm tra trạng thái:
```bash
docker compose -f docker-compose.image.yml ps
```
Xem log:
```bash
docker compose -f docker-compose.image.yml logs -f accmanager
```
---
## ✅ Kiểm tra & Troubleshoot
### Kiểm tra app chạy OK
```bash
# Kiểm tra container đang running
docker compose -f docker-compose.image.yml ps
# Xem log (tìm "Server running")
docker compose -f docker-compose.image.yml logs --tail=50 accmanager
### Nếu gặp lỗi
**Lỗi: "image not found"**
- Kiểm tra: `cat .env | grep DOCKER_IMAGE`
- Đảm bảo image đã push lên Docker Hub, kiểm tra: https://hub.docker.com/r/toiiiiday/accmanager
**Lỗi: "connection refused"**
- Kiểm tra DB Server có chạy: `ssh robotics@172.20.235.176`
- Kiểm tra DB credentials trong .env
**Lỗi: Container restart liên tục**
- Xem log: `docker compose -f docker-compose.image.yml logs --tail=100 accmanager`
---
## 🔄 Cập nhật bản mới
Áp dụng đúng 8 bước sau cho mỗi lần sửa code:
### Trên máy DEV
1. Chọn version mới (ví dụ `1.0.3`)
2. Build:
```powershell
docker build -t toiiiiday/accmanager:"version" .
```
3. Push:
```powershell
docker push toiiiiday/accmanager:"version"
```
4. Cập nhật `.env`:
```env
DOCKER_IMAGE=toiiiiday/accmanager:"version"
```
5. Copy `.env` lên server:
```powershell
scp .env robotics@172.20.235.176:~/accmanager/.env
```
### Trên máy SERVER
6. SSH và vào thư mục deploy:
```bash
ssh robotics@172.20.235.176
cd ~/accmanager
```
7. Pull + Up:
```bash
docker compose --env-file .env -f docker-compose.image.yml pull accmanager
docker compose --env-file .env -f docker-compose.image.yml up -d accmanager
```
8. Kiểm tra bản mới đã chạy:
```bash
docker compose -f docker-compose.image.yml ps
docker compose -f docker-compose.image.yml logs --tail=50 accmanager
```
### Có cần gắn tag/version cho mỗi phiên bản mới không?
**Có, nên làm bắt buộc cho production.**
Lý do:
1. Tránh đè image cũ và tránh nhầm lẫn khi deploy.
2. Rollback nhanh về bản ổn định trước đó.
3. Truy vết được bản code nào đang chạy trên server.
4. Tránh rủi ro do dùng `latest` (khó kiểm soát).
Quy ước khuyến nghị:
- `1.0.2` -> fix nhỏ
- `1.1.0` -> thêm tính năng
- `2.0.0` -> thay đổi lớn/breaking
Ví dụ rollback về bản cũ `1.0.2`:
```env
DOCKER_IMAGE=toiiiiday/accmanager:1.0.2
```
```bash
docker compose --env-file .env -f docker-compose.image.yml pull accmanager
docker compose --env-file .env -f docker-compose.image.yml up -d accmanager
```
---
## 🌐 Public Domain qua Nginx Proxy Manager
### Chuẩn bị
1. **Domain**
- Trỏ DNS A record về IP public nơi đặt Nginx Proxy Manager
2. **Firewall/Router**
- Mở inbound port 80, 443 từ Internet
- Port 3000 chỉ nội bộ (không public)
---
## 📝 Các file liên quan
- `docker-compose.yml` - Build local
- `docker-compose.image.yml` - Pull & run từ registry
- `.env` - Biến môi trường
- `.dockerignore` - Ignore file khi build
- `Dockerfile` - Config image
- `deploy-dev.ps1` - Script build & push (Windows)
- `deploy-server.sh` - Script pull & deploy (Linux)
---
## 🎯 Tóm tắt quy trình
```
Máy DEV
├─ Chỉnh sửa code
├─ docker build -t toiiiiday/accmanager:X .
├─ docker push toiiiiday/accmanager:X
├─ sửa DOCKER_IMAGE trong .env
└─ scp .env server (copy env)
Máy Server
├─ ssh vào server
├─ cd ~/accmanager
├─ docker compose pull accmanager
└─ docker compose up -d accmanager
Nginx Proxy Manager
└─ Forward từ domain → http://172.20.235.176:3000
```
---
## 💡 Mẹo
1. **Luôn tăng version tag**: 1.0.1 → 1.0.2 → 1.0.3
2. **Rollback nhanh**: Chỉ cần đổi DOCKER_IMAGE trong .env sang tag cũ rồi deploy lại
3. **Giữ log**: `docker compose logs --tail=1000 > backup.log`
4. **Restart container**: `docker compose -f docker-compose.image.yml restart accmanager`
5. **Xóa container cũ**: `docker compose -f docker-compose.image.yml down`
6. **Không dùng `latest` cho production**: luôn deploy bằng tag cụ thể
---
**Cần giúp? Xem log chi tiết:**
```bash
docker compose -f docker-compose.image.yml logs --tail=200 accmanager
```

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM node:20-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY backend ./backend
COPY public ./public
EXPOSE 3000
CMD ["node", "backend/server.js"]

186
README.md
View File

@@ -1,166 +1,62 @@
# 🎯 AccManager - SQL Server Backend Setup Complete # AccManager
## ✅ Database Configuration Complete AccManager là hệ thống giúp quản lý tập trung tài khoản truy cập cho nhân sự trong tổ chức.
SQL Server database **AccManager** has been successfully configured with all necessary tables and initial data. Thay vì lưu rải rác tài khoản ứng dụng ở nhiều nơi, AccManager gom tất cả về một màn hình để dễ tìm, dễ phân quyền và dễ kiểm soát.
### 📊 Database Information ## Dự án này dùng để làm gì
``` - Quản lý danh sách người dùng trong nội bộ.
Server IP: 172.20.235.176 - Quản lý danh sách ứng dụng đang sử dụng trong công ty.
Database: AccManager - Gán tài khoản truy cập ứng dụng cho từng người dùng.
User: sa - Theo dõi thông tin tài khoản rõ ràng, tránh thất lạc.
Password: robotics@2020
Port: 1433 (default)
```
### 👤 Default Admin Account ## Ai sẽ sử dụng
``` - Quản trị viên: tạo người dùng, tạo ứng dụng, cấp tài khoản, cập nhật thông tin.
Username: admin - Người dùng thông thường: xem các tài khoản được cấp cho mình.
Password: admin
Role: admin
Status: Active
```
## 📋 Database Tables Created ## Cách sử dụng nhanh
### 1. **Users** - User Management 1. Đăng nhập vào hệ thống.
- Stores login credentials and user roles 2. Vào mục Người dùng để tạo mới hoặc cập nhật thông tin nhân sự.
- Default admin user: admin/admin 3. Vào mục Ứng dụng để thêm các hệ thống cần quản lý.
4. Vào mục Tài khoản để gán tài khoản ứng dụng cho đúng người.
5. Dùng nút Xem chi tiết trong danh sách người dùng để kiểm tra đầy đủ thông tin trước khi chỉnh sửa.
### 2. **Applications** - Service Management ## Quy trình vận hành gợi ý
- 4 sample applications pre-loaded:
- AWS (Cloud) - online
- GitHub (VCS) - online
- Google Workspace (Collaboration) - online
- Nginx Proxy (Infra) - offline
### 3. **Accounts** - Credential Storage 1. Khi có nhân sự mới:
- Stores credentials for each user-application combination - Tạo người dùng.
- Linked to Users and Applications tables - Gán quyền phù hợp.
- Cấp các tài khoản ứng dụng cần thiết.
### 4. **AuditLog** - Activity Tracking 2. Khi thay đổi công việc:
- Logs all INSERT, UPDATE, DELETE operations - Cập nhật lại danh sách ứng dụng được cấp.
- User actions tracked for security - Thu hồi các tài khoản không còn sử dụng.
## 🚀 Backend Server Options 3. Khi nhân sự nghỉ việc:
- Khóa hoặc ngừng kích hoạt người dùng.
- Rà soát và thu hồi toàn bộ tài khoản liên quan.
### Option 1⃣: Node.js + Express (Recommended) ## Lợi ích chính
**Files:** - Giảm thất lạc thông tin tài khoản.
- `server.js` - Main server file - Rõ ràng trách nhiệm ai đang dùng tài khoản nào.
- `package.json` - Dependencies - Tiết kiệm thời gian bàn giao và thu hồi quyền truy cập.
- Dễ kiểm tra trước khi cấp quyền mới.
**Quick Start:** ## Tài liệu liên quan
```bash
# 1. Install Node.js from https://nodejs.org/
# 2. Install dependencies
npm install
# 3. Run server - Hướng dẫn triển khai: [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)
npm start
# Server runs on: http://localhost:3000 ## Cau hinh email xac thuc dang ky
```
### Option 2⃣: Python + Flask He thong da ho tro xac thuc email cho:
- Dang ky tai khoan moi.
- Doi email trong phan ho so ca nhan.
**Files:**
- `server_python.py` - Main server file
- `requirements.txt` - Dependencies
**Quick Start:**
```bash
# 1. Install Python 3.8+ from https://www.python.org/
# 2. Install dependencies
pip install -r requirements.txt
# 3. Run server Phiên bản tài liệu: 4.0.0
python server_python.py Cập nhật: Tháng 4 năm 2026
# Server runs on: http://localhost:5000
```
## 📡 API Endpoints
### Health Check
```http
GET /api/health
```
### Authentication
```http
POST /api/auth/login
```
### Users Management
```http
GET /api/users
GET /api/users/:id
POST /api/users
```
### Applications
```http
GET /api/applications
POST /api/applications
```
### Accounts
```http
GET /api/accounts/user/:userId
POST /api/accounts
```
### Database Info
```http
GET /api/database/info
```
## 📚 Documentation Files
- **README.md** (this file) - Overview
- **SETUP_GUIDE.md** - Detailed installation steps
- **DATABASE_SETUP.md** - Schema and API documentation
- **server.js** - Node.js backend source
- **server_python.py** - Python backend source
## 🔐 Default Credentials
```
Username: admin
Password: admin
Role: admin
```
## 🔧 Project Files
```
d:\RoboticsSource\AccManager\
├── server.js (Node.js backend)
├── server_python.py (Python backend)
├── package.json (Node.js dependencies)
├── requirements.txt (Python dependencies)
├── .env (Configuration)
├── database/
│ └── setup.sql (SQL setup script)
├── SETUP_GUIDE.md (Installation guide)
├── DATABASE_SETUP.md (Database documentation)
└── README.md (This file)
```
## ✅ Status
- ✓ Database created (AccManager)
- ✓ 4 tables created (Users, Applications, Accounts, AuditLog)
- ✓ Admin user created (admin/admin)
- ✓ Sample applications added
- ✓ Backend servers ready (Node.js + Python options)
- ✓ API endpoints documented
---
**Version:** 2.0.0 (Backend Ready)
**Database:** SQL Server / AccManager
**Last Updated:** March 27, 2026

View File

@@ -1,220 +0,0 @@
# 🚀 Hướng Dẫn Chạy AccManager - Dành cho Người Dùng Mới
## 📋 Bước 1: Kiểm Tra Node.js
Mở **PowerShell** và chạy:
```powershell
node --version
npm --version
```
**Kết quả mong đợi:**
```
v18.16.0 (hoặc version cao hơn)
9.6.7 (hoặc version khác)
```
Nếu thấy version → **OK, tiếp tục bước 2**
---
## 📋 Bước 2: Vào Thư Mục Dự Án
Copy-paste lệnh này vào PowerShell:
```powershell
cd d:\RoboticsSource\AccManager
```
---
## 📋 Bước 3: Cài Đặt Dependencies
Copy-paste lệnh này vào PowerShell:
```powershell
npm install
```
**Chờ cho tới khi thấy:**
```
added XXX packages in XXs
```
(Sẽ mất 1-3 phút tùy tốc độ mạng)
---
## 📋 Bước 4: Chạy Backend Server
Copy-paste lệnh này vào PowerShell:
```powershell
npm start
```
**Khi thành công, bạn sẽ thấy:**
```
========================================
AccManager Backend Server
========================================
✓ Server running on http://localhost:3000
✓ Database: AccManager
✓ Default admin: admin / admin
API Endpoints:
POST /api/auth/login
GET /api/database/info
GET /api/users
GET /api/applications
GET /api/accounts/user/:userId
========================================
```
**👉 ĐỪNG ĐÓNG TERMINAL NÀY - Server đang chạy!**
---
## 🌐 Bước 5: Mở Ứng Dụng
**Mở trình duyệt** (Chrome, Firefox, Edge...) copy-paste URL này:
```
http://localhost:3000/pages/login.html
```
---
## 🔐 Bước 6: Đăng Nhập
**Nhập thông tin:**
- Username: `admin`
- Password: `admin`
Nhấp **Login**
---
## ✅ Nếu Thành Công
Bạn sẽ thấy:
- ✓ Trang Dashboard
- ✓ Quản lý Accounts
- ✓ Quản lý Applications
- ✓ Thông tin Database
---
## ❌ Nếu Có Lỗi
### Lỗi 1: "npm: The term 'npm' is not recognized"
**Giải pháp:**
- Đóng PowerShell hiện tại
- Mở PowerShell **mới**
- Chạy lại `npm --version`
### Lỗi 2: "Cannot find module 'express'"
**Giải pháp:**
```powershell
npm install
npm start
```
### Lỗi 3: "Port 3000 already in use"
**Giải pháp:** Có chương trình khác dùng port 3000
```powershell
# Dừng server cũ (Ctrl+C)
# Rồi chạy lại: npm start
```
### Lỗi 4: "Cannot connect to database"
**Giải pháp:** Kiểm tra SQL Server
- Đảm bảo SQL Server đang chạy
- IP: 172.20.235.176 có reach được không
```powershell
ping 172.20.235.176
```
---
## 💡 Full Workflow - Copy Toàn Bộ Lệnh
Nếu bạn muốn copy toàn bộ một lần:
```powershell
# Kiểm tra Node.js
node --version
# Vào thư mục
cd d:\RoboticsSource\AccManager
# Cài packages
npm install
# Chạy server
npm start
```
---
## 🎯 Các Lệnh Hữu Ích Khác
### Dừng Server
Nhấp **Ctrl + C** trong PowerShell
### Chạy Server Ở Chế Độ Development (auto-restart)
```powershell
npm run dev
```
### Kiểm Tra API Health
Mở browser khác nhập:
```
http://localhost:3000/api/health
```
### Xem Tất Cả Users
```
http://localhost:3000/api/users
```
### Xem Tất Cả Applications
```
http://localhost:3000/api/applications
```
### Database Info
```
http://localhost:3000/api/database/info
```
---
## 📊 Sơ Đồ Port
| URL | Port | Mục đích |
|-----|------|---------|
| http://localhost:3000/pages/login.html | 3000 | Frontend |
| http://localhost:3000/api/health | 3000 | Backend API |
| 172.20.235.176 | 1433 | SQL Server |
---
## 🎯 Tóm Tắt - 4 Bước Chính
1. **Mở PowerShell**
2. **Chạy:**
```powershell
cd d:\RoboticsSource\AccManager
npm install
npm start
```
3. **Chờ thấy "Server running on http://localhost:3000"**
4. **Mở browser vào: http://localhost:3000/pages/login.html**
5. **Đăng nhập: admin / admin**
---
**Chúc bạn thành công! 🚀**

View File

@@ -1,294 +0,0 @@
# 🚀 AccManager Backend - Complete Setup Guide
## ⚠️ Pre-requisites
### 1. Install Node.js & npm
**Download từ:** https://nodejs.org/
**Khuyến khích:** LTS version (v18 hoặc mới hơn)
#### Kiểm tra installation:
```bash
node --version
npm --version
```
Expected output:
```
v18.* (or newer)
9.* (or newer)
```
### 2. Verify SQL Server Connection
Trước khi chạy backend, kiểm tra SQL Server:
```bash
ping 172.20.235.176
```
Nếu không ping được, kiểm tra:
- SQL Server đang chạy
- Firewall cho phép port 1433
- Network connectivity
---
## 📥 Setup Steps
### Step 1: Install Node Packages
```bash
cd d:\RoboticsSource\AccManager
npm install
```
Wait cho tới khi mô tả xuất hiện `added X packages`
### Step 2: Run Backend Server
```bash
npm start
```
**Expected Output:**
```
========================================
AccManager Backend Server
========================================
✓ Server running on http://localhost:3000
✓ Database: AccManager
✓ Default admin: admin / admin
API Endpoints:
POST /api/auth/login
GET /api/database/info
GET /api/users
GET /api/applications
GET /api/accounts/user/:userId
========================================
```
### Step 3: Test Connection
Mở terminal mới, chạy:
```bash
curl http://localhost:3000/api/health
```
**Response:**
```json
{
"status": "OK",
"database": "Connected"
}
```
### Step 4: Test Database Info
```bash
curl http://localhost:3000/api/database/info
```
---
## 📝 Database Structure
### Database Name: `AccManager`
#### Tables:
1. **Users** (1 admin account)
- Username: admin
- Password: admin
- Role: admin
2. **Applications** (4 sample apps)
- AWS
- GitHub
- Google Workspace
- Nginx Proxy
3. **Accounts** (empty, ready to use)
4. **AuditLog** (empty, for logging)
---
## 🧪 Test API Endpoints
### Test 1: Login
```bash
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d "{\"username\":\"admin\",\"password\":\"admin\"}"
```
### Test 2: Get Users
```bash
curl http://localhost:3000/api/users
```
### Test 3: Get Applications
```bash
curl http://localhost:3000/api/applications
```
### Test 4: View Database Info
```bash
curl http://localhost:3000/api/database/info
```
---
## 🔌 Frontend Integration
Update your frontend to connect to the backend:
### Option 1: Update app.js
```javascript
const API_URL = 'http://localhost:3000/api';
// Update login function
async function login(username, password) {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
localStorage.setItem('currentUser', JSON.stringify(data.user));
window.location.href = './pages/accounts.html';
} else {
alert('Login failed: ' + data.message);
}
}
// Update get accounts function
async function getAccounts(userId) {
const response = await fetch(`${API_URL}/accounts/user/${userId}`);
const data = await response.json();
return data.data || [];
}
```
---
## 📦 Project Structure
```
d:\RoboticsSource\AccManager\
├── server.js # Backend server (Node.js)
├── package.json # Dependencies
├── .env # Configuration
├── DATABASE_SETUP.md # This file
├── database/
│ └── setup.sql # SQL setup script
├── index.html # Frontend entry
├── pages/
│ ├── login.html
│ ├── accounts.html
│ ├── applications.html
│ └── index.html
└── js/
└── app.js # Frontend logic
```
---
## 🔒 Security Notes
⚠️ **For Development Only:**
- Admin password is hardcoded as "admin"
- SQL credentials in code (not recommended for production)
- CORS enabled for all origins
### For Production:
1. Use environment variables
2. Hash passwords with bcrypt
3. Implement JWT authentication
4. Use firewalls and VPNs
5. Enable SSL/TLS
---
## 🐛 Troubleshooting
### Error: "Cannot find module 'express'"
```bash
npm install
```
### Error: "Connection failed"
Check:
- SQL Server running
- IP address correct: 172.20.235.176
- Port 1433 accessible
- Username/password correct
### Error: "Port 3000 already in use"
```bash
# Change port in .env
PORT=3001
# Then restart server
npm start
```
### Error: "CORS error in browser"
This is normal during development. The backend already has CORS enabled.
---
## 📊 Database Credentials
```
Server: 172.20.235.176
Database: AccManager
User: sa
Password: robotics@2020
```
---
## ✅ Verification Checklist
- [ ] Node.js installed
- [ ] npm packages installed (`npm install`)
- [ ] Backend server running (`npm start`)
- [ ] Can access http://localhost:3000/api/health
- [ ] Can login with admin/admin
- [ ] Database shows tables and statistics
- [ ] Frontend connects to backend
---
## 📚 Additional Commands
### Install Development Tools
```bash
npm install -D nodemon
npm run dev # Auto-restart on code changes
```
### Check npm packages
```bash
npm list
```
### Update packages
```bash
npm update
```
### Clear npm cache
```bash
npm cache clean --force
```
---
**For Support:** Check server logs for error messages

4862
backend/server.js Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,63 +0,0 @@
# PowerShell Script to Execute SQL Server Setup
# Database: AccManager
# Server: 172.20.235.176
# SQL Server Connection Info
$ServerName = "172.20.235.176"
$Username = "sa"
$Password = "robotics@2022"
$SqlScriptPath = Join-Path $PSScriptRoot "setup.sql"
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "AccManager Database Setup" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Check if sqlcmd is available
if (-not (Get-Command sqlcmd -ErrorAction SilentlyContinue)) {
Write-Host "ERROR: sqlcmd is not available on this system." -ForegroundColor Red
Write-Host "Please install SQL Server Command Line Tools (sqlcmd)" -ForegroundColor Yellow
Write-Host "Download from: https://docs.microsoft.com/en-us/sql/tools/sqlcmd-utility" -ForegroundColor Yellow
exit
}
# Check if SQL script file exists
if (-not (Test-Path $SqlScriptPath)) {
Write-Host "ERROR: SQL script file not found: $SqlScriptPath" -ForegroundColor Red
exit
}
Write-Host "Server: $ServerName" -ForegroundColor Green
Write-Host "User: $Username" -ForegroundColor Green
Write-Host "Script: $SqlScriptPath" -ForegroundColor Green
Write-Host ""
# Execute SQL Script
Write-Host "Executing SQL script..." -ForegroundColor Yellow
Write-Host ""
try {
sqlcmd -S $ServerName -U $Username -P $Password -i $SqlScriptPath -o "setup_output.log"
Write-Host ""
Write-Host "========================================" -ForegroundColor Green
Write-Host "SETUP COMPLETED SUCCESSFULLY!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
Write-Host "Output saved to: setup_output.log" -ForegroundColor Cyan
Write-Host ""
# Display output
Write-Host "Setup Output:" -ForegroundColor Cyan
Get-Content "setup_output.log"
}
catch {
Write-Host "ERROR executing SQL script:" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
exit
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Green
Write-Host "Database is ready to use!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green

View File

@@ -81,7 +81,327 @@ BEGIN
END END
-- =========================================== -- ===========================================
-- 4. CREATE AUDIT LOG TABLE -- 4. CREATE ASSET INVENTORY TABLE
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetInventory')
BEGIN
CREATE TABLE AssetInventory (
AssetId INT PRIMARY KEY IDENTITY(1,1),
AssetCode NVARCHAR(100) NOT NULL UNIQUE,
AssetName NVARCHAR(255) NOT NULL,
Model NVARCHAR(255),
SerialNumber NVARCHAR(100),
Quantity INT NOT NULL DEFAULT 0,
ImportInPeriod INT NOT NULL DEFAULT 0,
ExportInPeriod INT NOT NULL DEFAULT 0,
EndingBalance INT NOT NULL DEFAULT 0,
NewQuantity INT NOT NULL DEFAULT 0,
UsedQuantity INT NOT NULL DEFAULT 0,
Unit NVARCHAR(50),
Department NVARCHAR(100),
Project NVARCHAR(150),
Location NVARCHAR(150),
Custodian NVARCHAR(100),
Borrower NVARCHAR(255),
ExportedBy NVARCHAR(100),
PurchaseDate DATE NULL,
PurchasePrice DECIMAL(18,2) NULL,
Status NVARCHAR(30) NOT NULL DEFAULT 'in_use',
Notes NVARCHAR(MAX),
CreatedBy INT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
);
PRINT 'Table AssetInventory created successfully.';
END
IF COL_LENGTH('dbo.AssetInventory', 'Borrower') IS NULL
BEGIN
ALTER TABLE AssetInventory ADD Borrower NVARCHAR(255) NULL;
END
IF COL_LENGTH('dbo.AssetInventory', 'ExportedBy') IS NULL
BEGIN
ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;
END
IF COL_LENGTH('dbo.AssetInventory', 'NewQuantity') IS NULL
BEGIN
ALTER TABLE AssetInventory ADD NewQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_NewQuantity DEFAULT(0);
END
IF COL_LENGTH('dbo.AssetInventory', 'UsedQuantity') IS NULL
BEGIN
ALTER TABLE AssetInventory ADD UsedQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_UsedQuantity DEFAULT(0);
END
UPDATE AssetInventory
SET EndingBalance = ISNULL(EndingBalance, ISNULL(Quantity, 0));
UPDATE AssetInventory
SET UsedQuantity = CASE WHEN ISNULL(UsedQuantity, 0) < 0 THEN 0 ELSE ISNULL(UsedQuantity, 0) END;
UPDATE AssetInventory
SET NewQuantity = CASE
WHEN ISNULL(NewQuantity, 0) < 0 THEN 0
ELSE ISNULL(NewQuantity, 0)
END;
UPDATE AssetInventory
SET NewQuantity = CASE
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) < ISNULL(EndingBalance, 0)
THEN ISNULL(NewQuantity, 0) + (ISNULL(EndingBalance, 0) - (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)))
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
THEN CASE
WHEN ISNULL(NewQuantity, 0) >= ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
THEN ISNULL(NewQuantity, 0) - ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
ELSE 0
END
ELSE ISNULL(NewQuantity, 0)
END,
UsedQuantity = CASE
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
AND ISNULL(NewQuantity, 0) < ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
THEN ISNULL(EndingBalance, 0)
ELSE ISNULL(UsedQuantity, 0)
END;
-- ===========================================
-- 5. CREATE ASSET DEPARTMENTS TABLE
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetDepartments')
BEGIN
CREATE TABLE AssetDepartments (
DepartmentId INT PRIMARY KEY IDENTITY(1,1),
DepartmentName NVARCHAR(100) NOT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
);
PRINT 'Table AssetDepartments created successfully.';
END
;WITH SourceDepartments AS (
SELECT DISTINCT LTRIM(RTRIM(Department)) AS DepartmentName
FROM AssetInventory
WHERE Department IS NOT NULL
AND LTRIM(RTRIM(Department)) <> ''
)
INSERT INTO AssetDepartments (DepartmentName)
SELECT source.DepartmentName
FROM SourceDepartments source
WHERE NOT EXISTS (
SELECT 1
FROM AssetDepartments target
WHERE LOWER(LTRIM(RTRIM(target.DepartmentName))) = LOWER(source.DepartmentName)
);
-- ===========================================
-- 6. CREATE ASSET PROJECTS TABLE
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetProjects')
BEGIN
CREATE TABLE AssetProjects (
ProjectId INT PRIMARY KEY IDENTITY(1,1),
ProjectName NVARCHAR(150) NOT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
);
PRINT 'Table AssetProjects created successfully.';
END
-- ===========================================
-- 7. CREATE ASSET BORROW REQUESTS TABLE
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequests')
BEGIN
CREATE TABLE AssetBorrowRequests (
BorrowId INT PRIMARY KEY IDENTITY(1,1),
AssetId INT NOT NULL,
RequestType NVARCHAR(20) NOT NULL DEFAULT 'borrow',
RequestStatus NVARCHAR(20) NOT NULL DEFAULT 'pending',
BorrowerName NVARCHAR(100) NOT NULL,
BorrowQuantity INT NOT NULL DEFAULT 1,
Unit NVARCHAR(50),
BorrowDate DATE NOT NULL DEFAULT CAST(GETDATE() AS DATE),
RequestNote NVARCHAR(500) NULL,
RejectReason NVARCHAR(1000) NULL,
CreatedBy INT NULL,
ProcessedBy INT NULL,
ProcessedByName NVARCHAR(100) NULL,
ProcessedDate DATETIME NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL,
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL
);
PRINT 'Table AssetBorrowRequests created successfully.';
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'Unit') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD Unit NVARCHAR(50) NULL;
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'BorrowDate') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(GETDATE() AS DATE));
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'UpdatedDate') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(GETDATE());
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RequestType') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD RequestType NVARCHAR(20) NOT NULL CONSTRAINT DF_AssetBorrowRequests_RequestType DEFAULT('borrow');
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RequestStatus') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD RequestStatus NVARCHAR(20) NOT NULL CONSTRAINT DF_AssetBorrowRequests_RequestStatus DEFAULT('approved');
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RequestNote') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD RequestNote NVARCHAR(500) NULL;
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RejectReason') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD RejectReason NVARCHAR(1000) NULL;
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'ProcessedBy') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD ProcessedBy INT NULL;
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'ProcessedByName') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD ProcessedByName NVARCHAR(100) NULL;
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'ProcessedDate') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD ProcessedDate DATETIME NULL;
END
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_AssetBorrowRequests_ProcessedBy')
BEGIN
ALTER TABLE AssetBorrowRequests
ADD CONSTRAINT FK_AssetBorrowRequests_ProcessedBy
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL;
END
UPDATE AssetBorrowRequests
SET RequestType = ISNULL(NULLIF(LTRIM(RTRIM(RequestType)), ''), 'borrow');
UPDATE AssetBorrowRequests
SET RequestStatus = ISNULL(NULLIF(LTRIM(RTRIM(RequestStatus)), ''), 'approved');
-- ===========================================
-- 8. CREATE ASSET EXPORT HISTORY TABLE
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetExportHistory')
BEGIN
CREATE TABLE AssetExportHistory (
ExportHistoryId INT PRIMARY KEY IDENTITY(1,1),
AssetId INT NOT NULL,
AssetCode NVARCHAR(100) NOT NULL,
AssetName NVARCHAR(255) NOT NULL,
ExportQuantity INT NOT NULL DEFAULT 1,
ProjectName NVARCHAR(150) NULL,
CustodianName NVARCHAR(100) NOT NULL,
ExportedByName NVARCHAR(100) NOT NULL,
ExportNote NVARCHAR(1000) NULL,
CreatedBy INT NULL,
ExportedDate DATETIME NOT NULL DEFAULT GETDATE(),
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE
);
PRINT 'Table AssetExportHistory created successfully.';
END
IF COL_LENGTH('dbo.AssetExportHistory', 'AssetCode') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD AssetCode NVARCHAR(100) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'AssetName') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD AssetName NVARCHAR(255) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportQuantity') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD ExportQuantity INT NOT NULL CONSTRAINT DF_AssetExportHistory_ExportQuantity DEFAULT(1);
END
IF COL_LENGTH('dbo.AssetExportHistory', 'ProjectName') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD ProjectName NVARCHAR(150) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'CustodianName') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD CustodianName NVARCHAR(100) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportedByName') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD ExportedByName NVARCHAR(100) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportNote') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD ExportNote NVARCHAR(1000) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'CreatedBy') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD CreatedBy INT NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportedDate') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_ExportedDate DEFAULT(GETDATE());
END
IF COL_LENGTH('dbo.AssetExportHistory', 'CreatedDate') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_CreatedDate DEFAULT(GETDATE());
END
IF COL_LENGTH('dbo.AssetExportHistory', 'UpdatedDate') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_UpdatedDate DEFAULT(GETDATE());
END
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_AssetExportHistory_CreatedBy')
AND COL_LENGTH('dbo.AssetExportHistory', 'CreatedBy') IS NOT NULL
BEGIN
IF NOT EXISTS (
SELECT 1
FROM sys.foreign_key_columns fkc
INNER JOIN sys.columns c
ON c.object_id = fkc.parent_object_id
AND c.column_id = fkc.parent_column_id
WHERE fkc.parent_object_id = OBJECT_ID('dbo.AssetExportHistory')
AND c.name = 'CreatedBy'
)
BEGIN
ALTER TABLE AssetExportHistory
ADD CONSTRAINT FK_AssetExportHistory_CreatedBy
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL;
END
END
-- ===========================================
-- 9. CREATE AUDIT LOG TABLE
-- =========================================== -- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog') IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
BEGIN BEGIN
@@ -100,7 +420,7 @@ BEGIN
END END
-- =========================================== -- ===========================================
-- 5. CREATE INDEXES -- 10. CREATE INDEXES
-- =========================================== -- ===========================================
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username') IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
BEGIN BEGIN
@@ -117,10 +437,65 @@ BEGIN
CREATE INDEX IX_Accounts_AppId ON Accounts(AppId); CREATE INDEX IX_Accounts_AppId ON Accounts(AppId);
END END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_AssetCode')
BEGIN
CREATE INDEX IX_AssetInventory_AssetCode ON AssetInventory(AssetCode);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_Status')
BEGIN
CREATE INDEX IX_AssetInventory_Status ON AssetInventory(Status);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_Department')
BEGIN
CREATE INDEX IX_AssetInventory_Department ON AssetInventory(Department);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'UX_AssetDepartments_DepartmentName')
BEGIN
CREATE UNIQUE INDEX UX_AssetDepartments_DepartmentName ON AssetDepartments(DepartmentName);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'UX_AssetProjects_ProjectName')
BEGIN
CREATE UNIQUE INDEX UX_AssetProjects_ProjectName ON AssetProjects(ProjectName);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_AssetId')
BEGIN
CREATE INDEX IX_AssetBorrowRequests_AssetId ON AssetBorrowRequests(AssetId);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_BorrowDate')
BEGIN
CREATE INDEX IX_AssetBorrowRequests_BorrowDate ON AssetBorrowRequests(BorrowDate DESC);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_RequestStatus')
BEGIN
CREATE INDEX IX_AssetBorrowRequests_RequestStatus ON AssetBorrowRequests(RequestStatus);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_RequestType')
BEGIN
CREATE INDEX IX_AssetBorrowRequests_RequestType ON AssetBorrowRequests(RequestType);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetExportHistory_AssetId')
BEGIN
CREATE INDEX IX_AssetExportHistory_AssetId ON AssetExportHistory(AssetId);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetExportHistory_ExportedDate')
BEGIN
CREATE INDEX IX_AssetExportHistory_ExportedDate ON AssetExportHistory(ExportedDate DESC);
END
PRINT 'Indexes created successfully.'; PRINT 'Indexes created successfully.';
-- =========================================== -- ===========================================
-- 6. INSERT INITIAL DATA -- 11. INSERT INITIAL DATA
-- =========================================== -- ===========================================
-- Check if admin user exists -- Check if admin user exists
@@ -144,7 +519,7 @@ BEGIN
END END
-- =========================================== -- ===========================================
-- 7. DISPLAY DATABASE INFORMATION -- 12. DISPLAY DATABASE INFORMATION
-- =========================================== -- ===========================================
PRINT ''; PRINT '';
PRINT '========================================'; PRINT '========================================';

View File

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

26
docker-compose.image.yml Normal file
View File

@@ -0,0 +1,26 @@
services:
accmanager:
image: ${DOCKER_IMAGE:-yourdockerhub/accmanager:latest}
container_name: accmanager
restart: unless-stopped
ports:
- "${APP_PORT:-3000}:3000"
environment:
NODE_ENV: ${NODE_ENV:-production}
PORT: 3000
DB_SERVER: ${DB_SERVER:-172.20.235.176}
DB_USER: ${DB_USER:-sa}
DB_PASSWORD: ${DB_PASSWORD:-changeme}
DB_NAME: ${DB_NAME:-AccManager}
DB_ENCRYPT: ${DB_ENCRYPT:-false}
DB_TRUST_CERTIFICATE: ${DB_TRUST_CERTIFICATE:-true}
DB_CONNECT_TIMEOUT: ${DB_CONNECT_TIMEOUT:-30000}
BCRYPT_ROUNDS: ${BCRYPT_ROUNDS:-12}
APP_BASE_URL: ${APP_BASE_URL:-http://localhost:3000}
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_SECURE: ${SMTP_SECURE:-false}
SMTP_USER: ${SMTP_USER:-}
SMTP_PASS: ${SMTP_PASS:-}
SMTP_FROM: ${SMTP_FROM:-}
EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30}

28
docker-compose.yml Normal file
View File

@@ -0,0 +1,28 @@
services:
accmanager:
build:
context: .
dockerfile: Dockerfile
container_name: accmanager
restart: unless-stopped
ports:
- "${APP_PORT:-3000}:3000"
environment:
NODE_ENV: production
PORT: 3000
DB_SERVER: ${DB_SERVER:-172.20.235.176}
DB_USER: ${DB_USER:-sa}
DB_PASSWORD: ${DB_PASSWORD:-changeme}
DB_NAME: ${DB_NAME:-AccManager}
DB_ENCRYPT: ${DB_ENCRYPT:-false}
DB_TRUST_CERTIFICATE: ${DB_TRUST_CERTIFICATE:-true}
DB_CONNECT_TIMEOUT: ${DB_CONNECT_TIMEOUT:-30000}
BCRYPT_ROUNDS: ${BCRYPT_ROUNDS:-12}
APP_BASE_URL: ${APP_BASE_URL:-http://localhost:3000}
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_SECURE: ${SMTP_SECURE:-false}
SMTP_USER: ${SMTP_USER:-}
SMTP_PASS: ${SMTP_PASS:-}
SMTP_FROM: ${SMTP_FROM:-}
EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30}

View File

@@ -1,134 +0,0 @@
# Robot Account Manager - Project Structure
## Directory Organization
```
AccManager/
├── pages/ # HTML pages
│ ├── login.html # Login page (entry point)
│ ├── index.html # Dashboard
│ ├── accounts.html # Accounts management
│ └── applications.html # Applications management
├── js/ # JavaScript files
│ └── app.js # Main application logic
├── docs/ # Documentation
│ └── README.md # This file
├── index.html # Root redirect to pages/login.html
├── .git/ # Version control
├── .vscode/ # VS Code settings
└── README.md # Project root README
```
## Quick Start
1. **Access the application:**
- Open `index.html` in a browser
- You will be automatically redirected to `pages/login.html`
2. **Login Credentials (Demo):**
- Username: `admin`
- Password: `admin`
3. **Features:**
- Dashboard: Overview of accounts and applications
- Accounts: Manage user accounts for various services
- Applications: Manage connected applications/services
## File Structure Explanation
### /pages/
Contains all HTML pages with updated relative paths for script imports:
- `login.html` - Authentication page
- `index.html` - Main dashboard
- `accounts.html` - Accounts management interface
- `applications.html` - Applications management interface
All pages reference scripts using `../js/app.js` for correct path resolution.
### /js/
Application logic and state management:
- `app.js` - Main AccountManager class with all functionality
### /docs/
Documentation files for reference and development.
## Application Architecture
### AccountManager Class (app.js)
- **Storage**: Uses localStorage for data persistence
- **Pages**: Dynamically renders content based on user navigation
- **Authentication**: Checks for valid session on page load
- **Modals**: Manages adding/editing accounts and applications
### Data Storage
Application stores data in browser's localStorage:
- `currentUser` - Logged-in user information
- `accounts` - List of managed accounts
- `applications` - List of managed applications
- `rememberedUsername` - Optional saved username
## Development Notes
- **Framework**: Tailwind CSS for styling
- **Icons**: Material Design symbols
- **Storage**: Browser localStorage (client-side only)
- **Responsive**: Built with mobile-first approach
- **Modern**: ES6+ JavaScript features
## Navigation Flow
```
index.html (root)
pages/login.html
↓ (after login)
pages/index.html (dashboard)
├→ pages/accounts.html
└→ pages/applications.html
```
## Features
### Authentication
- Username/password login
- Session management
- Remember me functionality
- Logout with confirmation
### Dashboard
- Overview statistics
- Recent account activity
- Quick access to management pages
### Account Management
- Add new accounts
- Edit existing accounts
- Delete accounts
- Filter by service
- View account details
### Application Management
- Add new applications
- Edit application details
- Delete applications
- View application status (online/offline)
- Health monitoring
## Browser Compatibility
- Modern browsers (Chrome, Firefox, Safari, Edge)
- Requires JavaScript enabled
- Uses localStorage API
## Future Improvements
- Server-side authentication
- Database integration
- Advanced filtering and search
- User roles and permissions
- Audit logging
- Dark mode toggle persistence
---
Generated: March 27, 2026
Version: 1.0.0

View File

@@ -1,35 +0,0 @@
# Install Node.js automatically (Run as Administrator)
Write-Host "Installing Node.js LTS..." -ForegroundColor Green
# Check if winget is available
if (Get-Command winget -ErrorAction SilentlyContinue) {
Write-Host "Found winget, installing Node.js..." -ForegroundColor Cyan
winget install OpenJS.NodeJS
}
else {
Write-Host "winget not found. Downloading Node.js installer..." -ForegroundColor Yellow
# Download Node.js LTS
$NodeURL = "https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi"
$InstallerPath = "$env:TEMP\nodejs-installer.msi"
Write-Host "Downloading from: $NodeURL"
Invoke-WebRequest -Uri $NodeURL -OutFile $InstallerPath -UseBasicParsing
Write-Host "Running installer..." -ForegroundColor Cyan
Start-Process -FilePath "msiexec.exe" -ArgumentList "/i $InstallerPath /quiet /norestart" -Wait
Remove-Item $InstallerPath
}
Write-Host "Installation complete! Refreshing environment..." -ForegroundColor Green
# Refresh PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
Write-Host "Checking installation..." -ForegroundColor Cyan
node --version
npm --version
Write-Host "Ready to run npm install!" -ForegroundColor Green

830
js/app.js
View File

@@ -1,830 +0,0 @@
// VaultSentinel - Account Management Application
// Main JavaScript functionality
class AccountManager {
constructor() {
// Check if user is logged in
const currentUser = this.loadFromStorage('currentUser');
if (!currentUser) {
window.location.href = '../pages/login.html';
return;
}
this.currentUser = currentUser;
this.accounts = [];
this.applications = [];
this.apiBase = '/api';
this.currentPage = 'dashboard';
this.initPromise = this.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.loadModals(); // Load modals từ file riêng
// Render dashboard content (only for index.html)
const mainContent = document.getElementById('mainContent');
if (mainContent && window.location.pathname.endsWith('index.html')) {
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() {
// Modal close buttons
document.querySelectorAll('[data-close-modal]').forEach(btn => {
btn.addEventListener('click', () => this.closeModals());
});
// Close with Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.closeModals();
}
});
// Form submissions
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));
}
// Logout button
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', () => this.handleLogout());
}
// Update account display
this.updateAccountDisplay();
// Account table row clicks
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() {
// Use the logged-in user from constructor
const usernameEl = document.getElementById('accountUsername');
const roleEl = document.getElementById('accountRole');
if (usernameEl) usernameEl.textContent = this.currentUser?.username || this.currentUser?.Username || 'User';
if (roleEl) roleEl.textContent = this.currentUser?.role || this.currentUser?.Role || 'Administrator';
}
handleLogout() {
if (confirm('Are you sure you want to logout?')) {
this.saveToStorage('currentUser', null);
localStorage.clear();
window.location.href = '../pages/login.html';
}
}
renderDashboard() {
return `
<div class="flex-1 flex flex-col p-6 space-y-6 min-h-0 overflow-auto">
<!-- Title and Stats -->
<div class="flex items-end justify-between shrink-0">
<div>
<h1 class="text-2xl font-extrabold text-on-surface tracking-tight leading-none">System Overview</h1>
<p class="text-xs text-on-surface-variant font-medium mt-1">Account & Service Management</p>
</div>
<div class="flex gap-2">
<a href="./accounts.html" class="py-1.5 px-3 bg-primary text-on-primary rounded-lg text-xs font-bold flex items-center gap-1.5 shadow-sm active:scale-95 duration-100">
<span class="material-symbols-outlined text-sm">add</span>
Add Account
</a>
</div>
</div>
<!-- Metric Grid -->
<div class="grid grid-cols-4 gap-4 shrink-0">
<div class="bg-surface-container-lowest p-4 rounded-xl border border-outline-variant/15 flex flex-col">
<span class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider mb-2">Applications</span>
<div class="flex items-baseline justify-between">
<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 || a.status) === 'online').length} Active</span>
</div>
</div>
<div class="bg-surface-container-lowest p-4 rounded-xl border border-outline-variant/15 flex flex-col">
<span class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider mb-2">Total Accounts</span>
<div class="flex items-baseline justify-between">
<span class="text-2xl font-black text-on-surface">${this.accounts.length}</span>
<span class="text-[10px] font-bold text-on-surface-variant/40">Managed</span>
</div>
</div>
<div class="bg-surface-container-lowest p-4 rounded-xl border border-outline-variant/15 flex flex-col">
<span class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider mb-2">Last Updated</span>
<div class="flex items-baseline justify-between">
<span class="text-sm font-black text-on-surface">${new Date().toLocaleDateString()}</span>
</div>
</div>
<div class="bg-primary-container/10 p-4 rounded-xl border border-primary/20 flex flex-col">
<span class="text-[10px] font-bold text-primary uppercase tracking-wider mb-2">Status</span>
<div class="flex items-baseline justify-between">
<span class="text-lg font-black text-primary">Operational</span>
<span class="material-symbols-outlined text-primary text-sm">check_circle</span>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="bg-surface-container-lowest rounded-xl p-5 border border-outline-variant/15 flex flex-col flex-1 min-h-0">
<div class="flex items-center justify-between mb-4 shrink-0">
<h3 class="text-sm font-bold text-on-surface flex items-center gap-2">
<span class="material-symbols-outlined text-primary text-base">history</span>
Recent Accounts
</h3>
</div>
${this.accounts.length > 0 ? `
<div class="flex-1 overflow-y-auto space-y-2">
${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-1 min-w-0">
<p class="text-[11px] font-bold text-on-surface truncate">${username}</p>
<p class="text-[9px] text-on-surface-variant">${service}${owner}</p>
</div>
</div>
`;}).join('')}
</div>
` : `
<div class="flex-1 flex items-center justify-center text-center">
<p class="text-sm text-on-surface-variant">No accounts yet. <a href="./accounts.html" class="text-primary font-bold">Create one</a></p>
</div>
`}
</div>
</div>
`;
}
getAccountsContent() {
return `
<div class="p-4 md:p-6 flex flex-col h-full overflow-hidden">
<!-- Page Header -->
<div class="flex items-center justify-between mb-4 shrink-0">
<div>
<h1 class="text-xl font-extrabold text-on-surface tracking-tight">Accounts Management</h1>
<p class="text-[10px] text-on-surface-variant uppercase font-semibold tracking-widest mt-0.5">Administrative Access Control</p>
</div>
<button id="addAccountBtn" class="bg-primary hover:bg-primary-dim text-on-primary px-3 py-1.5 rounded-lg text-xs font-bold shadow-sm flex items-center gap-1.5 transition-all active:scale-95">
<span class="material-symbols-outlined text-base">person_add</span>
Add Account
</button>
</div>
<!-- Filters -->
<div class="flex items-center gap-3 mb-4">
<div class="flex items-center gap-1.5">
<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">
<option value="">All Services</option>
${this.applications.map(app => `<option value="${app.AppId}">${app.Name}</option>`).join('')}
</select>
</div>
<div class="flex-1"></div>
</div>
<!-- Accounts Table -->
<div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
${this.accounts.length > 0 ? `
<div class="overflow-y-auto overflow-x-auto flex-1">
<table class="w-full text-left border-collapse w-full">
<thead class="sticky top-0 z-10">
<tr class="bg-slate-50 border-b border-slate-200">
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Owner</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Username</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Service</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
${this.accounts.map(acc => `
<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.Email || '-'}</td>
<td class="px-4 py-3 text-sm text-slate-600">${acc.AccountUsername || '-'}</td>
<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.AppName || '-'}</span>
</td>
<td class="px-4 py-3 text-right">
<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>
</button>
<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>
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
` : `
<div class="flex-1 flex items-center justify-center text-center">
<div>
<p class="text-sm text-on-surface-variant mb-4">No accounts yet. Create one to get started.</p>
<button id="addAccountBtn" class="bg-primary hover:bg-primary-dim text-on-primary px-4 py-2 rounded-lg text-sm font-bold">
<span class="material-symbols-outlined text-base inline mr-2">person_add</span>
Add First Account
</button>
</div>
</div>
`}
</div>
</div>
`;
}
getApplicationsContent() {
return `
<div class="flex flex-col p-6 overflow-hidden h-full">
<!-- Header Section -->
<div class="flex items-center justify-between gap-6 mb-6 shrink-0">
<div>
<h1 class="text-2xl font-extrabold text-on-surface tracking-tight">Applications</h1>
<p class="text-sm text-on-surface-variant">Manage and monitor active infrastructure services.</p>
</div>
<button id="addAppBtn" class="bg-primary hover:bg-primary-dim text-on-primary px-4 py-2 rounded-xl font-bold flex items-center gap-2 transition-all active:scale-95 shadow-sm">
<span class="material-symbols-outlined">add</span>
Add New
</button>
</div>
<!-- Dashboard Stats -->
<div class="grid grid-cols-3 gap-4 mb-6 shrink-0">
<div class="bg-surface-container-lowest px-4 py-3 rounded-xl shadow-sm border border-outline-variant/5 flex items-center gap-4">
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
<span class="material-symbols-outlined text-xl">lan</span>
</div>
<div>
<p class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider">Active</p>
<p class="text-lg font-black text-on-surface">${this.applications.filter(a => a.status === 'online').length}</p>
</div>
</div>
<div class="bg-surface-container-lowest px-4 py-3 rounded-xl shadow-sm border border-outline-variant/5 flex items-center gap-4">
<div class="w-10 h-10 rounded-lg bg-tertiary/10 flex items-center justify-center text-tertiary">
<span class="material-symbols-outlined text-xl">bolt</span>
</div>
<div>
<p class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider">Total</p>
<p class="text-lg font-black text-on-surface">${this.applications.length}</p>
</div>
</div>
<div class="bg-surface-container-lowest px-4 py-3 rounded-xl shadow-sm border border-outline-variant/5 flex items-center gap-4">
<div class="w-10 h-10 rounded-lg bg-secondary/10 flex items-center justify-center text-secondary">
<span class="material-symbols-outlined text-xl">database</span>
</div>
<div>
<p class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider">Health</p>
<p class="text-lg font-black text-on-surface">99.9%</p>
</div>
</div>
</div>
<!-- Applications List -->
<div class="bg-surface-container-lowest rounded-xl shadow-sm border border-outline-variant/10 overflow-hidden flex flex-col flex-1 min-h-0">
<div class="overflow-y-auto flex-1">
<table class="w-full text-left border-collapse">
<thead class="sticky top-0 bg-surface-container-lowest z-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">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 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-outline-variant/5">
${this.applications.map(app => `
<tr class="hover:bg-surface-container-low/30 transition-colors group">
<td class="px-6 py-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">
<span class="material-symbols-outlined text-sm">${app.Icon || 'apps'}</span>
</div>
<span class="font-bold text-sm text-on-surface">${app.Name}</span>
</div>
</td>
<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>
</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">
<div class="flex items-center gap-1.5">
<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 || app.status) === 'online' ? 'text-on-primary-fixed-variant' : 'text-error'}">${(app.Status || app.status) === 'online' ? 'Online' : 'Offline'}</span>
</div>
</td>
<td class="px-6 py-3 text-right">
<div class="flex items-center justify-end gap-1">
<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>
</button>
<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>
</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
}
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 => {
btn.addEventListener('click', (e) => {
const accountId = Number(btn.dataset.accountId);
const account = this.accounts.find(a => a.AccountId === accountId);
this.pendingDeleteAccountId = accountId;
document.getElementById('deleteAccountUsername').textContent = account?.AccountUsername || '';
document.getElementById('deleteAccountModal').classList.add('open');
});
});
// Confirm Delete Account
document.querySelectorAll('.confirm-delete-account').forEach(btn => {
btn.addEventListener('click', () => {
if (this.pendingDeleteAccountId !== undefined) {
fetch(`${this.apiBase}/accounts/${this.pendingDeleteAccountId}`, { method: 'DELETE' })
.then(res => res.json())
.then(data => {
if (data.success) {
alert('Account deleted successfully');
this.closeModals();
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 => {
btn.addEventListener('click', (e) => {
const accountId = Number(btn.dataset.accountId);
const account = this.accounts.find(a => a.AccountId === accountId);
// Populate form with existing data
const form = document.getElementById('accountForm');
if (form) {
const userInput = form.querySelector('#accountUsername');
const passInput = form.querySelector('#accountPassword');
const ownerInput = form.querySelector('#accountOwner');
const serviceSelect = form.querySelector('#accountService');
if (userInput) userInput.value = account?.AccountUsername || '';
if (passInput) passInput.value = account?.AccountPassword || '';
if (ownerInput) ownerInput.value = this.currentUser?.Username || this.currentUser?.username || '';
if (serviceSelect) serviceSelect.value = account?.AppId || '';
}
this.editingAccountId = account?.AccountId;
this.closeModals();
this.openAccountModal();
});
});
// Edit from View modal
document.querySelectorAll('.edit-account-from-view').forEach(btn => {
btn.addEventListener('click', () => {
const account = this.currentViewAccount;
const form = document.getElementById('accountForm');
if (form) {
const userInput = form.querySelector('#accountUsername');
const passInput = form.querySelector('#accountPassword');
const ownerInput = form.querySelector('#accountOwner');
const serviceSelect = form.querySelector('#accountService');
if (userInput) userInput.value = account?.AccountUsername || '';
if (passInput) passInput.value = account?.AccountPassword || '';
if (ownerInput) ownerInput.value = this.currentUser?.Username || this.currentUser?.username || '';
if (serviceSelect) serviceSelect.value = account?.AppId || '';
}
this.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 => {
btn.addEventListener('click', (e) => {
const appId = Number(btn.dataset.appId);
const app = this.applications.find(a => a.AppId === appId);
document.getElementById('appName').value = app?.Name || '';
document.getElementById('appType').value = app?.Type || '';
document.getElementById('appStatus').value = app?.Status || 'online';
document.getElementById('appDescription').value = app?.Description || '';
document.getElementById('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();
});
});
}
handleAccountSubmit(e) {
e.preventDefault();
const accountForm = document.getElementById('accountForm');
const userId = this.getUserId();
const appId = Number(accountForm?.querySelector('#accountService')?.value || 0);
const accountUsername = (accountForm?.querySelector('#accountUsername')?.value || '').trim();
const accountPassword = (accountForm?.querySelector('#accountPassword')?.value || '').trim();
const accountEmail = ((accountForm?.querySelector('#accountOwner')?.value || '').trim()) || this.currentUser?.Username || this.currentUser?.username || '';
if (!accountForm) {
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: ''
};
const isEdit = this.editingAccountId !== undefined;
const url = isEdit ? `${this.apiBase}/accounts/${this.editingAccountId}` : `${this.apiBase}/accounts`;
const method = isEdit ? 'PUT' : 'POST';
fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(res => res.json()).then(data => {
if (data.success) {
this.editingAccountId = undefined;
alert(isEdit ? 'Account updated successfully' : 'Account created successfully');
this.closeModals();
location.href = './accounts.html';
} else {
alert(data.message || 'Save account failed');
}
}).catch(err => {
console.error(err);
alert('Save account failed');
});
}
handleAppSubmit(e) {
e.preventDefault();
const payload = {
name: document.getElementById('appName').value,
type: document.getElementById('appType').value,
status: document.getElementById('appStatus').value,
icon: 'cloud',
description: document.getElementById('appDescription')?.value || '',
url: (document.getElementById('appUrl')?.value || '').trim()
};
const isEdit = this.editingAppId !== undefined;
const url = isEdit ? `${this.apiBase}/applications/${this.editingAppId}` : `${this.apiBase}/applications`;
const method = isEdit ? 'PUT' : 'POST';
fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(res => res.json()).then(data => {
if (data.success) {
this.editingAppId = undefined;
alert(isEdit ? 'Application updated successfully' : 'Application created successfully');
this.closeModals();
location.href = './applications.html';
} else {
alert(data.message || 'Save application failed');
}
}).catch(err => {
console.error(err);
alert('Save application failed');
});
}
openAccountModal() {
// Refresh service options so newly added applications appear
const serviceSelect = document.getElementById('accountService');
if (serviceSelect) {
serviceSelect.innerHTML = `<option value="">Select a service</option>` +
this.applications.map(app => `<option value="${app.AppId}">${app.Name}</option>`).join('');
}
if (this.editingAccountId === undefined) {
const form = document.getElementById('accountForm');
if (form) {
const serviceSelect = form.querySelector('#accountService');
const ownerInput = form.querySelector('#accountOwner');
const userInput = form.querySelector('#accountUsername');
const passInput = form.querySelector('#accountPassword');
if (serviceSelect) serviceSelect.value = '';
if (ownerInput) ownerInput.value = this.currentUser?.Username || this.currentUser?.username || '';
if (userInput) userInput.value = '';
if (passInput) passInput.value = '';
}
}
document.getElementById('accountModal').classList.add('open');
}
openAppModal() {
if (this.editingAppId === undefined) {
document.getElementById('appName').value = '';
document.getElementById('appType').value = '';
document.getElementById('appStatus').value = 'online';
const desc = document.getElementById('appDescription');
const url = document.getElementById('appUrl');
if (desc) desc.value = '';
if (url) url.value = '';
}
document.getElementById('appModal').classList.add('open');
}
closeModals() {
document.querySelectorAll('.modal-backdrop').forEach(modal => {
modal.classList.remove('open');
});
}
loadFromStorage(key) {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
}
saveToStorage(key, data) {
localStorage.setItem(key, JSON.stringify(data));
}
}
// 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
let app;
document.addEventListener('DOMContentLoaded', () => {
app = new AccountManager();
});

View File

@@ -1,187 +0,0 @@
<!-- 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>

1215
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,21 +2,37 @@
"name": "accmanager-backend", "name": "accmanager-backend",
"version": "1.0.0", "version": "1.0.0",
"description": "Backend server for AccManager application", "description": "Backend server for AccManager application",
"main": "server.js", "main": "backend/server.js",
"scripts": { "scripts": {
"start": "node server.js", "start": "node backend/server.js",
"dev": "nodemon server.js" "dev": "nodemon backend/server.js",
"build:css": "tailwindcss -c tailwind.config.js -i ./public/css/tailwind.css -o ./public/css/main.css --minify",
"watch:css": "tailwindcss -c tailwind.config.js -i ./public/css/tailwind.css -o ./public/css/main.css --watch"
}, },
"keywords": ["accmanager", "backend", "express", "mssql"], "keywords": [
"accmanager",
"backend",
"express",
"mssql"
],
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bcrypt": "^6.0.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"mssql": "^9.1.1", "mssql": "^9.1.1",
"cors": "^2.8.5", "multer": "^2.1.1",
"dotenv": "^16.3.1" "nodemailer": "^8.0.4",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.0.1" "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.7",
"autoprefixer": "^10.4.19",
"nodemon": "^3.0.1",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.13"
} }
} }

View File

@@ -1,248 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>VaultSentinel - Accounts Management</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"on-secondary-fixed-variant": "#4e5c71",
"on-secondary": "#f8f8ff",
"secondary-fixed-dim": "#c7d5ed",
"surface-variant": "#d9e4ea",
"surface-tint": "#3755c3",
"primary-container": "#dde1ff",
"primary-dim": "#2848b7",
"on-background": "#2a3439",
"surface-container-lowest": "#ffffff",
"tertiary-fixed-dim": "#d4cdee",
"on-tertiary-container": "#514d68",
"error-container": "#fe8983",
"on-secondary-container": "#455367",
"outline": "#717c82",
"on-primary": "#f8f7ff",
"on-primary-container": "#2747b6",
"inverse-primary": "#6d89fa",
"on-surface": "#2a3439",
"primary-fixed": "#dde1ff",
"on-primary-fixed": "#0732a3",
"secondary-dim": "#465468",
"surface-container-high": "#e1e9ee",
"surface-container-highest": "#d9e4ea",
"on-primary-fixed-variant": "#3352c0",
"on-error-container": "#752121",
"secondary": "#526074",
"tertiary-fixed": "#e3dbfd",
"primary": "#3755c3",
"surface-dim": "#cfdce3",
"tertiary": "#605c78",
"on-error": "#fff7f6",
"secondary-fixed": "#d5e3fc",
"error-dim": "#4e0309",
"surface-bright": "#f7f9fb",
"on-surface-variant": "#566166",
"on-tertiary": "#fcf7ff",
"tertiary-container": "#e3dbfd",
"inverse-on-surface": "#9a9d9f",
"on-tertiary-fixed-variant": "#5b5672",
"tertiary-dim": "#54506b",
"outline-variant": "#a9b4b9",
"on-secondary-fixed": "#324053",
"inverse-surface": "#0b0f10",
"on-tertiary-fixed": "#3e3a54",
"primary-fixed-dim": "#cad2ff",
"surface-container": "#e8eff3",
"secondary-container": "#d5e3fc",
"surface-container-low": "#f0f4f7",
"background": "#f7f9fb",
"error": "#9f403d",
"surface": "#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-family: 'Material Symbols Outlined'; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20; font-size: 1.25rem; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-flex; white-space: nowrap; word-wrap: normal; direction: ltr; }
body { font-family: 'Inter', sans-serif; height: 100vh; overflow: hidden; }
h1, h2, h3, .brand-logo { font-family: 'Manrope', sans-serif; }
.modal-backdrop { opacity: 0; transition: opacity 0.2s ease-in-out; pointer-events: none; }
.modal-backdrop.open { opacity: 1; pointer-events: auto; }
.modal-content { transform: scale(0.95); transition: transform 0.2s ease-in-out; }
.modal-backdrop.open .modal-content { transform: scale(1); }
</style>
</head>
<body class="bg-background text-on-surface antialiased flex h-screen w-screen">
<aside class="h-screen w-56 flex flex-col bg-slate-100 dark:bg-slate-900 font-manrope text-sm font-medium border-r border-outline-variant/10 shrink-0">
<div class="flex flex-col h-full py-6">
<div class="px-6 mb-8">
<div class="text-lg font-black text-slate-900 dark:text-slate-50 tracking-tight leading-none">Robot Account</div>
<div class="text-[10px] uppercase tracking-widest text-on-surface-variant mt-1.5 font-bold">Admin Console</div>
</div>
<nav class="flex-1 px-3 space-y-1">
<a href="./index.html" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer">
<span class="material-symbols-outlined">dashboard</span>
<span>Dashboard</span>
</a>
<a href="./applications.html" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer">
<span class="material-symbols-outlined">apps</span>
<span>Applications</span>
</a>
<a href="./accounts.html" class="flex items-center gap-3 px-3 py-2 border-l-4 border-blue-600 bg-slate-200/80 dark:bg-slate-800 text-slate-900 dark:text-slate-50 font-bold group transition-all cursor-pointer">
<span class="material-symbols-outlined">manage_accounts</span>
<span>Accounts</span>
</a>
</nav>
<div class="px-6 pt-4 border-t border-outline-variant/10">
<div class="text-[10px] font-bold text-on-surface-variant/40 uppercase tracking-widest">v1.0.0</div>
</div>
</div>
</aside>
<main class="flex-1 flex flex-col h-screen min-w-0">
<header class="h-14 flex items-center justify-between px-6 bg-slate-50/80 dark:bg-slate-950/80 backdrop-blur-xl border-b border-outline-variant/10 shrink-0">
<div class="flex items-center gap-4 flex-1">
<div class="flex items-center bg-surface-container-high px-3 py-1.5 rounded-full w-64 group focus-within:ring-2 ring-primary/20 transition-all">
<span class="material-symbols-outlined text-on-surface-variant text-base">search</span>
<input id="searchInput" class="bg-transparent border-none focus:ring-0 text-xs w-full placeholder:text-on-surface-variant/60 py-0" placeholder="Search resources..." type="text"/>
</div>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800">
<span class="material-symbols-outlined text-slate-600 dark:text-slate-400">account_circle</span>
<div class="flex flex-col">
<span id="accountUsername" class="text-xs font-semibold text-slate-900 dark:text-slate-50">User Account</span>
<span id="accountRole" class="text-[10px] text-slate-500 dark:text-slate-400">Administrator</span>
</div>
</div>
<button id="logoutBtn" class="p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-red-100 dark:hover:bg-red-950 hover:text-red-700 dark:hover:text-red-300 transition-colors" title="Logout">
<span class="material-symbols-outlined">logout</span>
</button>
</div>
</header>
<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>
<script src="../js/app.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (app) {
const mainContent = document.getElementById('mainContent');
const modalsContainer = document.getElementById('modalsContainer');
await app.initPromise;
if (mainContent) {
mainContent.innerHTML = app.getAccountsContent();
app.setupAccountRowListeners();
app.setupAddButtonListeners();
}
}
});
</script>
</body>
</html>

View File

@@ -1,235 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>VaultSentinel - Applications Management</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"on-secondary-fixed-variant": "#4e5c71",
"on-secondary": "#f8f8ff",
"secondary-fixed-dim": "#c7d5ed",
"surface-variant": "#d9e4ea",
"surface-tint": "#3755c3",
"primary-container": "#dde1ff",
"primary-dim": "#2848b7",
"on-background": "#2a3439",
"surface-container-lowest": "#ffffff",
"tertiary-fixed-dim": "#d4cdee",
"on-tertiary-container": "#514d68",
"error-container": "#fe8983",
"on-secondary-container": "#455367",
"outline": "#717c82",
"on-primary": "#f8f7ff",
"on-primary-container": "#2747b6",
"inverse-primary": "#6d89fa",
"on-surface": "#2a3439",
"primary-fixed": "#dde1ff",
"on-primary-fixed": "#0732a3",
"secondary-dim": "#465468",
"surface-container-high": "#e1e9ee",
"surface-container-highest": "#d9e4ea",
"on-primary-fixed-variant": "#3352c0",
"on-error-container": "#752121",
"secondary": "#526074",
"tertiary-fixed": "#e3dbfd",
"primary": "#3755c3",
"surface-dim": "#cfdce3",
"tertiary": "#605c78",
"on-error": "#fff7f6",
"secondary-fixed": "#d5e3fc",
"error-dim": "#4e0309",
"surface-bright": "#f7f9fb",
"on-surface-variant": "#566166",
"on-tertiary": "#fcf7ff",
"tertiary-container": "#e3dbfd",
"inverse-on-surface": "#9a9d9f",
"on-tertiary-fixed-variant": "#5b5672",
"tertiary-dim": "#54506b",
"outline-variant": "#a9b4b9",
"on-secondary-fixed": "#324053",
"inverse-surface": "#0b0f10",
"on-tertiary-fixed": "#3e3a54",
"primary-fixed-dim": "#cad2ff",
"surface-container": "#e8eff3",
"secondary-container": "#d5e3fc",
"surface-container-low": "#f0f4f7",
"background": "#f7f9fb",
"error": "#9f403d",
"surface": "#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-family: 'Material Symbols Outlined'; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20; font-size: 1.25rem; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-flex; white-space: nowrap; word-wrap: normal; direction: ltr; }
body { font-family: 'Inter', sans-serif; height: 100vh; overflow: hidden; }
h1, h2, h3, .brand-logo { font-family: 'Manrope', sans-serif; }
.modal-backdrop { opacity: 0; transition: opacity 0.2s ease-in-out; pointer-events: none; }
.modal-backdrop.open { opacity: 1; pointer-events: auto; }
.modal-content { transform: scale(0.95); transition: transform 0.2s ease-in-out; }
.modal-backdrop.open .modal-content { transform: scale(1); }
</style>
</head>
<body class="bg-background text-on-surface antialiased flex h-screen w-screen">
<aside class="h-screen w-56 flex flex-col bg-slate-100 dark:bg-slate-900 font-manrope text-sm font-medium border-r border-outline-variant/10 shrink-0">
<div class="flex flex-col h-full py-6">
<div class="px-6 mb-8">
<div class="text-lg font-black text-slate-900 dark:text-slate-50 tracking-tight leading-none">Robot Account</div>
<div class="text-[10px] uppercase tracking-widest text-on-surface-variant mt-1.5 font-bold">Admin Console</div>
</div>
<nav class="flex-1 px-3 space-y-1">
<a href="./index.html" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer">
<span class="material-symbols-outlined">dashboard</span>
<span>Dashboard</span>
</a>
<a href="./applications.html" class="flex items-center gap-3 px-3 py-2 border-l-4 border-blue-600 bg-slate-200/80 dark:bg-slate-800 text-slate-900 dark:text-slate-50 font-bold group transition-all cursor-pointer">
<span class="material-symbols-outlined">apps</span>
<span>Applications</span>
</a>
<a href="./accounts.html" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer">
<span class="material-symbols-outlined">manage_accounts</span>
<span>Accounts</span>
</a>
</nav>
<div class="px-6 pt-4 border-t border-outline-variant/10">
<div class="text-[10px] font-bold text-on-surface-variant/40 uppercase tracking-widest">v1.0.0</div>
</div>
</div>
</aside>
<main class="flex-1 flex flex-col h-screen min-w-0">
<header class="h-14 flex items-center justify-between px-6 bg-slate-50/80 dark:bg-slate-950/80 backdrop-blur-xl border-b border-outline-variant/10 shrink-0">
<div class="flex items-center gap-4 flex-1">
<div class="flex items-center bg-surface-container-high px-3 py-1.5 rounded-full w-64 group focus-within:ring-2 ring-primary/20 transition-all">
<span class="material-symbols-outlined text-on-surface-variant text-base">search</span>
<input id="searchInput" class="bg-transparent border-none focus:ring-0 text-xs w-full placeholder:text-on-surface-variant/60 py-0" placeholder="Search resources..." type="text"/>
</div>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800">
<span class="material-symbols-outlined text-slate-600 dark:text-slate-400">account_circle</span>
<div class="flex flex-col">
<span id="accountUsername" class="text-xs font-semibold text-slate-900 dark:text-slate-50">User Account</span>
<span id="accountRole" class="text-[10px] text-slate-500 dark:text-slate-400">Administrator</span>
</div>
</div>
<button id="logoutBtn" class="p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-red-100 dark:hover:bg-red-950 hover:text-red-700 dark:hover:text-red-300 transition-colors" title="Logout">
<span class="material-symbols-outlined">logout</span>
</button>
</div>
</header>
<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>
<script src="../js/app.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (app) {
const mainContent = document.getElementById('mainContent');
await app.initPromise;
if (mainContent) {
mainContent.innerHTML = app.getApplicationsContent();
app.setupAccountRowListeners();
app.setupAddButtonListeners();
}
}
});
</script>
</body>
</html>

View File

@@ -1,176 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Robot Manager Account - Account Management System</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"on-secondary-fixed-variant": "#4e5c71",
"on-secondary": "#f8f8ff",
"secondary-fixed-dim": "#c7d5ed",
"surface-variant": "#d9e4ea",
"surface-tint": "#3755c3",
"primary-container": "#dde1ff",
"primary-dim": "#2848b7",
"on-background": "#2a3439",
"surface-container-lowest": "#ffffff",
"tertiary-fixed-dim": "#d4cdee",
"on-tertiary-container": "#514d68",
"error-container": "#fe8983",
"on-secondary-container": "#455367",
"outline": "#717c82",
"on-primary": "#f8f7ff",
"on-primary-container": "#2747b6",
"inverse-primary": "#6d89fa",
"on-surface": "#2a3439",
"primary-fixed": "#dde1ff",
"on-primary-fixed": "#0732a3",
"secondary-dim": "#465468",
"surface-container-high": "#e1e9ee",
"surface-container-highest": "#d9e4ea",
"on-primary-fixed-variant": "#3352c0",
"on-error-container": "#752121",
"secondary": "#526074",
"tertiary-fixed": "#e3dbfd",
"primary": "#3755c3",
"surface-dim": "#cfdce3",
"tertiary": "#605c78",
"on-error": "#fff7f6",
"secondary-fixed": "#d5e3fc",
"error-dim": "#4e0309",
"surface-bright": "#f7f9fb",
"on-surface-variant": "#566166",
"on-tertiary": "#fcf7ff",
"tertiary-container": "#e3dbfd",
"inverse-on-surface": "#9a9d9f",
"on-tertiary-fixed-variant": "#5b5672",
"tertiary-dim": "#54506b",
"outline-variant": "#a9b4b9",
"on-secondary-fixed": "#324053",
"inverse-surface": "#0b0f10",
"on-tertiary-fixed": "#3e3a54",
"primary-fixed-dim": "#cad2ff",
"surface-container": "#e8eff3",
"secondary-container": "#d5e3fc",
"surface-container-low": "#f0f4f7",
"background": "#f7f9fb",
"error": "#9f403d",
"surface": "#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-family: 'Material Symbols Outlined';
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
font-size: 1.25rem;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-flex;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}
body { font-family: 'Inter', sans-serif; height: 100vh; overflow: hidden; }
h1, h2, h3, .brand-logo { font-family: 'Manrope', sans-serif; }
.modal-backdrop {
opacity: 0;
transition: opacity 0.2s ease-in-out;
pointer-events: none;
}
.modal-backdrop.open {
opacity: 1;
pointer-events: auto;
}
.modal-content {
transform: scale(0.95);
transition: transform 0.2s ease-in-out;
}
.modal-backdrop.open .modal-content {
transform: scale(1);
}
</style>
</head>
<body class="bg-background text-on-surface antialiased flex h-screen w-screen">
<!-- SideNavBar -->
<aside class="h-screen w-56 flex flex-col bg-slate-100 dark:bg-slate-900 font-manrope text-sm font-medium border-r border-outline-variant/10 shrink-0">
<div class="flex flex-col h-full py-6">
<!-- Header -->
<div class="px-6 mb-8">
<div class="text-lg font-black text-slate-900 dark:text-slate-50 tracking-tight leading-none">Robot Account</div>
<div class="text-[10px] uppercase tracking-widest text-on-surface-variant mt-1.5 font-bold">Admin Console</div>
</div>
<!-- Primary Nav -->
<nav class="flex-1 px-3 space-y-1">
<a href="#" onclick="location.reload(); return false;" class="flex items-center gap-3 px-3 py-2 border-l-4 border-blue-600 bg-slate-200/80 dark:bg-slate-800 text-slate-900 dark:text-slate-50 font-bold group transition-all cursor-pointer">
<span class="material-symbols-outlined">dashboard</span>
<span>Dashboard</span>
</a>
<a href="./applications.html" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer">
<span class="material-symbols-outlined">apps</span>
<span>Applications</span>
</a>
<a href="./accounts.html" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer">
<span class="material-symbols-outlined">manage_accounts</span>
<span>Accounts</span>
</a>
</nav>
<!-- Footer -->
<div class="px-6 pt-4 border-t border-outline-variant/10">
<div class="text-[10px] font-bold text-on-surface-variant/40 uppercase tracking-widest">v1.0.0</div>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 flex flex-col h-screen min-w-0">
<!-- TopAppBar -->
<header class="h-14 flex items-center justify-between px-6 bg-slate-50/80 dark:bg-slate-950/80 backdrop-blur-xl border-b border-outline-variant/10 shrink-0">
<div class="flex items-center gap-4 flex-1">
<div class="flex items-center bg-surface-container-high px-3 py-1.5 rounded-full w-64 group focus-within:ring-2 ring-primary/20 transition-all">
<span class="material-symbols-outlined text-on-surface-variant text-base">search</span>
<input id="searchInput" class="bg-transparent border-none focus:ring-0 text-xs w-full placeholder:text-on-surface-variant/60 py-0" placeholder="Search resources..." type="text"/>
</div>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800">
<span class="material-symbols-outlined text-slate-600 dark:text-slate-400">account_circle</span>
<div class="flex flex-col">
<span id="accountUsername" class="text-xs font-semibold text-slate-900 dark:text-slate-50">User Account</span>
<span id="accountRole" class="text-[10px] text-slate-500 dark:text-slate-400">Administrator</span>
</div>
</div>
<button id="logoutBtn" class="p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-red-100 dark:hover:bg-red-950 hover:text-red-700 dark:hover:text-red-300 transition-colors" title="Logout">
<span class="material-symbols-outlined">logout</span>
</button>
</div>
</header>
<!-- Main Content Area -->
<div id="mainContent" class="flex-1 overflow-hidden">
<!-- Content will be rendered here by JavaScript -->
</div>
</main>
<script src="../js/app.js"></script>
</body>
</html>

View File

@@ -1,254 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>VaultSentinel - Login</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"on-secondary-fixed-variant": "#4e5c71",
"on-secondary": "#f8f8ff",
"secondary-fixed-dim": "#c7d5ed",
"surface-variant": "#d9e4ea",
"surface-tint": "#3755c3",
"primary-container": "#dde1ff",
"primary-dim": "#2848b7",
"on-background": "#2a3439",
"surface-container-lowest": "#ffffff",
"tertiary-fixed-dim": "#d4cdee",
"on-tertiary-container": "#514d68",
"error-container": "#fe8983",
"on-secondary-container": "#455367",
"outline": "#717c82",
"on-primary": "#f8f7ff",
"on-primary-container": "#2747b6",
"inverse-primary": "#6d89fa",
"on-surface": "#2a3439",
"primary-fixed": "#dde1ff",
"on-primary-fixed": "#0732a3",
"secondary-dim": "#465468",
"surface-container-high": "#e1e9ee",
"surface-container-highest": "#d9e4ea",
"on-primary-fixed-variant": "#3352c0",
"on-error-container": "#752121",
"secondary": "#526074",
"tertiary-fixed": "#e3dbfd",
"primary": "#3755c3",
"surface-dim": "#cfdce3",
"tertiary": "#605c78",
"on-error": "#fff7f6",
"secondary-fixed": "#d5e3fc",
"error-dim": "#4e0309",
"surface-bright": "#f7f9fb",
"on-surface-variant": "#566166",
"on-tertiary": "#fcf7ff",
"tertiary-container": "#e3dbfd",
"inverse-on-surface": "#9a9d9f",
"on-tertiary-fixed-variant": "#5b5672",
"tertiary-dim": "#54506b",
"outline-variant": "#a9b4b9",
"on-secondary-fixed": "#324053",
"inverse-surface": "#0b0f10",
"on-tertiary-fixed": "#3e3a54",
"primary-fixed-dim": "#cad2ff",
"surface-container": "#e8eff3",
"secondary-container": "#d5e3fc",
"surface-container-low": "#f0f4f7",
"background": "#f7f9fb",
"error": "#9f403d",
"surface": "#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-family: 'Material Symbols Outlined';
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
font-size: 1.25rem;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-flex;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}
body { font-family: 'Inter', sans-serif; min-height: 100vh; }
h1, h2, h3 { font-family: 'Manrope', sans-serif; }
</style>
</head>
<body class="bg-gradient-to-br from-blue-50 via-background to-purple-50 dark:from-slate-950 dark:via-slate-900 dark:to-slate-950 antialiased">
<div class="min-h-screen flex items-center justify-center px-4">
<div class="w-full max-w-md">
<!-- Login Card -->
<div class="bg-white dark:bg-slate-900 rounded-2xl shadow-lg border border-outline-variant/10 p-8">
<!-- Header -->
<div class="text-center mb-8">
<div class="flex justify-center mb-4">
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center">
<span class="material-symbols-outlined text-primary text-4xl">security</span>
</div>
</div>
<h1 class="text-2xl font-black text-slate-900 dark:text-slate-50 tracking-tight">VaultSentinel</h1>
<p class="text-xs uppercase tracking-widest text-on-surface-variant font-bold mt-2">Account Management System</p>
</div>
<!-- Login Form -->
<form id="loginForm" class="space-y-5">
<!-- Username/Email Input -->
<div>
<label for="username" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Username or Email</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">person</span>
</span>
<input
type="text"
id="username"
name="username"
placeholder="Enter your username"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<!-- Password Input -->
<div>
<label for="password" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Password</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">lock</span>
</span>
<input
type="password"
id="password"
name="password"
placeholder="Enter your password"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<!-- Remember Me Checkbox -->
<div class="flex items-center">
<input
type="checkbox"
id="remember"
name="remember"
class="w-4 h-4 rounded border-outline-variant/30 text-primary focus:ring-2 focus:ring-primary/50 cursor-pointer"
/>
<label for="remember" class="ml-2.5 text-xs font-medium text-on-surface-variant cursor-pointer">Remember me</label>
</div>
<!-- Login Button -->
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-6"
>
<span class="material-symbols-outlined text-sm">login</span>
<span>Sign In</span>
</button>
<!-- Error Message -->
<div id="errorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
</form>
<!-- Footer -->
<div class="mt-8 pt-6 border-t border-outline-variant/10 text-center">
<p class="text-[10px] text-on-surface-variant/60">Default credentials for demo: admin / admin</p>
</div>
</div>
<!-- Bottom info -->
<div class="text-center mt-6">
<p class="text-xs text-on-surface-variant/60">v1.0.0</p>
</div>
</div>
</div>
<script>
// Simple login functionality
const loginForm = document.getElementById('loginForm');
const errorMessage = document.getElementById('errorMessage');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const rememberCheckbox = document.getElementById('remember');
// Demo credentials
const validCredentials = {
username: 'admin',
password: 'admin'
};
// Check if already logged in
document.addEventListener('DOMContentLoaded', () => {
const currentUser = localStorage.getItem('currentUser');
if (currentUser) {
window.location.href = './index.html';
}
// Restore remembered username
const rememberedUsername = localStorage.getItem('rememberedUsername');
if (rememberedUsername) {
usernameInput.value = rememberedUsername;
rememberCheckbox.checked = true;
}
});
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
errorMessage.classList.add('hidden');
const username = usernameInput.value.trim();
const password = passwordInput.value;
// Validate credentials
if (username === validCredentials.username && password === validCredentials.password) {
// Store user info
const userData = {
username: username,
role: 'Administrator',
loginTime: new Date().toISOString()
};
localStorage.setItem('currentUser', JSON.stringify(userData));
// Handle remember me
if (rememberCheckbox.checked) {
localStorage.setItem('rememberedUsername', username);
} else {
localStorage.removeItem('rememberedUsername');
}
// Redirect to dashboard
window.location.href = './index.html';
} else {
// Show error
errorMessage.textContent = 'Invalid username or password. Try admin / admin';
errorMessage.classList.remove('hidden');
passwordInput.value = '';
passwordInput.focus();
}
});
</script>
</body>
</html>

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

1
public/css/main.css Normal file

File diff suppressed because one or more lines are too long

3
public/css/tailwind.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

7288
public/js/app.js Normal file

File diff suppressed because it is too large Load Diff

670
public/modals.html Normal file
View File

@@ -0,0 +1,670 @@
<!-- 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">Icon (Material Symbol)</label>
<input type="text" id="appIcon" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="apps, cloud, security">
<p class="text-[10px] text-slate-400 mt-1">Nhập tên icon từ https://fonts.google.com/icons (ví dụ: cloud, apps, security).</p>
</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">Icon</label>
<div id="viewAppIcon" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600 flex items-center gap-2">
<span class="material-symbols-outlined text-base" id="viewAppIconSymbol">apps</span>
<span id="viewAppIconName" class="text-sm text-slate-600">apps</span>
</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>
<!-- Add/Edit Asset Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetModal">
<div class="modal-content w-full max-w-2xl bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4 flex flex-col" style="max-height: calc(100vh - 2rem);">
<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">Biểu mẫu tài sản</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="assetForm" class="p-6 space-y-4 overflow-y-auto">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label id="assetCodeLabel" class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Mã tài sản</label>
<input type="text" id="assetCodeInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="De trong de he thong tu tao">
<p id="assetCodeHint" class="mt-1 text-xs text-slate-500">Để trống khi thêm mới, hệ thống sẽ tự tạo mã.</p>
<p id="assetCodeError" class="mt-1 text-xs font-semibold text-red-600 hidden"></p>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên tài sản <span class="text-red-600">*</span></label>
<input type="text" id="assetNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Laptop Dell Latitude 5440">
<p id="assetNameError" class="mt-1 text-xs font-semibold text-red-600 hidden"></p>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Trạng thái</label>
<input type="text" id="assetStatusInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly value="Trong kho">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Model</label>
<input type="text" id="assetModelInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="Latitude 5440">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số serial</label>
<input type="text" id="assetSerialInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="SN123...">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số lượng (Tồn đầu kỳ)</label>
<input type="number" id="assetQuantityInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="0">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Nhập trong kỳ</label>
<input type="number" id="assetImportInPeriodInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="0">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Xuất trong kỳ</label>
<input type="number" id="assetExportInPeriodInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" value="0" readonly>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tồn cuối kỳ</label>
<input type="number" id="assetEndingBalanceInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" value="0" readonly>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Đơn vị</label>
<input type="text" id="assetUnitInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="cái">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Phòng ban</label>
<select id="assetDepartmentInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
<option value="">-- Chọn phòng ban --</option>
</select>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Dự án</label>
<select id="assetProjectInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
<option value="">-- Chọn dự án --</option>
</select>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Vị trí</label>
<input type="text" id="assetLocationInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="Kho A">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Người phụ trách</label>
<select id="assetCustodianInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
<option value="">-- Chon nguoi phu trach --</option>
</select>
</div>
<div class="md:col-span-2">
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Người mượn hiện tại</label>
<textarea id="assetBorrowerSummaryInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-24 resize-none bg-slate-50" readonly></textarea>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ngày mua</label>
<input type="date" id="assetPurchaseDateInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Giá mua</label>
<input type="number" id="assetPriceInput" min="0" step="0.01" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="0">
</div>
<div class="md:col-span-2">
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ghi chú</label>
<textarea id="assetNotesInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none" placeholder="Thông tin bổ sung"></textarea>
</div>
</div>
<div class="flex gap-3 pt-2 sticky bottom-0 bg-white pb-1">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeAssetModal()">Hủy</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">Lưu tài sản</button>
</div>
</form>
</div>
</div>
<!-- Export Asset Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="borrowAssetModal">
<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">Xuất tài sản</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeBorrowAssetModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="borrowAssetForm" class="p-6 space-y-4">
<input type="hidden" id="borrowAssetIdInput">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tài sản</label>
<input type="text" id="borrowAssetNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tồn cuối kỳ hiện tại</label>
<input type="number" id="borrowCurrentEndingInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số lượng xuất</label>
<input type="number" id="borrowQuantityInput" min="1" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="1" required>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Người mượn</label>
<select id="borrowAssetUserInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
<option value="">-- Chọn người mượn --</option>
</select>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Xuất cho dự án</label>
<select id="borrowAssetProjectInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
<option value="">-- Chọn dự án --</option>
</select>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Người xuất</label>
<input type="text" id="borrowByInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
</div>
<div class="md:col-span-2">
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Role người thao tác</label>
<input type="text" id="borrowRoleInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
</div>
<div class="md:col-span-2">
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ghi chú xuất</label>
<textarea id="borrowAssetNoteInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none" placeholder="Nhập ghi chú (nếu có)"></textarea>
</div>
</div>
<div class="flex gap-3 pt-2">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeBorrowAssetModal()">Hủy</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">Xác nhận xuất</button>
</div>
</form>
</div>
</div>
<!-- Asset Export History Modal -->
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetExportHistoryModal" style="z-index: 125;">
<div class="modal-content w-full max-w-6xl bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4 flex flex-col" style="max-height: calc(100vh - 2rem);">
<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">Lịch sử xuất tài sản</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetExportHistoryModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<div class="p-6 pt-4 overflow-auto">
<div class="rounded-xl border border-slate-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse" style="min-width: 1100px;">
<thead class="bg-slate-50 border-b border-slate-200">
<tr>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ngày giờ</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Tài sản</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Số lượng</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Dự án</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Người phụ trách</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Người xuất</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ghi chú</th>
</tr>
</thead>
<tbody id="assetExportHistoryTableBody" class="divide-y divide-slate-100">
<tr>
<td colspan="7" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có dữ liệu lịch sử xuất.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Asset Borrow Request Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetBorrowRequestModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-visible m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 id="assetBorrowRequestModalTitle" class="text-base font-extrabold text-slate-900">Tạo đơn mượn tài sản</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetBorrowRequestModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="assetBorrowRequestForm" class="p-6 space-y-4 relative">
<input type="hidden" id="assetBorrowRequestTypeInput" value="borrow">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên đầy đủ</label>
<input type="text" id="assetBorrowRequesterInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên tài sản</label>
<div id="assetBorrowProductPicker" class="relative z-[130]">
<input type="hidden" id="assetBorrowProductInput">
<button
type="button"
id="assetBorrowProductDisplayBtn"
class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 text-left flex items-center justify-between gap-2"
>
<span id="assetBorrowProductDisplayText" class="flex-1 min-w-0 truncate text-slate-600">-- Chọn tài sản --</span>
<span class="material-symbols-outlined text-base text-slate-400">expand_more</span>
</button>
<div
id="assetBorrowProductDropdown"
class="hidden absolute left-0 right-0 w-full max-w-full mt-1 bg-white border border-slate-200 rounded-lg shadow-xl z-[140] overflow-hidden"
>
<div class="p-2 border-b border-slate-100">
<input
type="text"
id="assetBorrowProductSearchInput"
class="w-full border border-slate-200 rounded-md text-sm py-2 px-2.5"
placeholder="Tìm theo mã hoặc tên tài sản..."
autocomplete="off"
>
</div>
<div
id="assetBorrowProductList"
class="overflow-auto"
style="max-height: 224px; overflow: auto; overscroll-behavior: contain;"
></div>
<div id="assetBorrowProductLoading" class="hidden px-3 py-2 text-xs text-slate-500 bg-slate-50 border-t border-slate-100">
Đang tải thêm tài sản...
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số lượng</label>
<input type="number" id="assetBorrowQuantityInput" min="1" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="1" required>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Đơn vị</label>
<input type="text" id="assetBorrowUnitInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
</div>
</div>
<div>
<label id="assetBorrowDateLabel" class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ngày mượn</label>
<input type="date" id="assetBorrowDateInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ghi chú</label>
<textarea id="assetBorrowNoteInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none" placeholder="Nhập ghi chú (nếu có)"></textarea>
</div>
<div class="flex gap-3 pt-2">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeAssetBorrowRequestModal()">Hủy</button>
<button type="submit" id="assetBorrowRequestSubmitBtn" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Tạo đơn mượn</button>
</div>
</form>
</div>
</div>
<!-- Pending Asset Requests Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetPendingRequestsModal" style="z-index: 120;">
<div class="modal-content w-full max-w-6xl bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4 flex flex-col" style="max-height: calc(100vh - 2rem);">
<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">Đơn chờ xử lý</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetPendingRequestsModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<div class="p-6 grid grid-cols-1 md:grid-cols-2 gap-4 overflow-y-auto">
<div class="rounded-xl border border-slate-200 overflow-hidden">
<div class="px-4 py-3 bg-blue-50 border-b border-blue-100 flex items-center justify-between">
<h4 class="text-sm font-extrabold text-blue-700">Đơn mượn</h4>
<span id="pendingBorrowCountBadge" class="inline-flex items-center justify-center min-w-[24px] h-6 px-2 rounded-full bg-blue-600 text-white text-xs font-extrabold">0</span>
</div>
<div id="pendingBorrowRequestsList" class="p-3 space-y-3 max-h-[60vh] overflow-y-auto"></div>
</div>
<div class="rounded-xl border border-slate-200 overflow-hidden">
<div class="px-4 py-3 bg-emerald-50 border-b border-emerald-100 flex items-center justify-between">
<h4 class="text-sm font-extrabold text-emerald-700">Đơn trả</h4>
<span id="pendingReturnCountBadge" class="inline-flex items-center justify-center min-w-[24px] h-6 px-2 rounded-full bg-emerald-600 text-white text-xs font-extrabold">0</span>
</div>
<div id="pendingReturnRequestsList" class="p-3 space-y-3 max-h-[60vh] overflow-y-auto"></div>
</div>
</div>
</div>
</div>
<!-- Confirm Delete/Cancel Asset Request Modal -->
<div class="modal-backdrop fixed inset-0 z-[120] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetRequestDeleteConfirmModal" style="z-index: 140;">
<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">Xác nhận thao tác</h3>
</div>
<div class="p-6">
<p id="assetRequestDeleteConfirmMessage" class="text-sm text-slate-600 mb-6">Bạn có chắc muốn thực hiện thao tác này?</p>
<div class="flex gap-3">
<button type="button" class="cancel-asset-request-delete-confirm flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg">Đóng</button>
<button type="button" id="confirmAssetRequestDeleteBtn" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold">Xóa đơn</button>
</div>
</div>
</div>
</div>
<!-- Reject Asset Request Modal -->
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetRequestRejectModal" style="z-index: 130;">
<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">Từ chối đơn</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetRequestRejectModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="assetRequestRejectForm" class="p-6 space-y-4">
<input type="hidden" id="assetRequestRejectIdInput">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Lý do từ chối</label>
<textarea id="assetRequestRejectReasonInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-24 resize-none" placeholder="Nhập lý do từ chối..." required></textarea>
</div>
<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="closeAssetRequestRejectModal()">Hủy</button>
<button type="submit" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold">Xác nhận từ chối</button>
</div>
</form>
</div>
</div>
<!-- View Asset Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="viewAssetModal">
<div class="modal-content w-full max-w-2xl bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4 flex flex-col" style="max-height: calc(100vh - 2rem);">
<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">Chi tiết tài sản</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeViewAssetModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<div id="assetDetailsContent" class="p-6 grid grid-cols-1 md:grid-cols-2 gap-3 text-sm text-slate-700 overflow-y-auto"></div>
<div class="p-6 pt-3 flex gap-3 border-t border-slate-100 bg-white">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeViewAssetModal()">Đóng</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-asset-from-view">Sửa</button>
</div>
</div>
</div>
<!-- Delete Asset Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteAssetModal">
<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">Xóa tài sản</h3>
</div>
<div class="p-6">
<p class="text-sm text-slate-600 mb-6">Bạn có chắc muốn xóa tài sản <strong id="deleteAssetName">-</strong>? Hành động này không thể hoàn tác.</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="closeDeleteAssetModal()">Hủy</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-asset">Xóa</button>
</div>
</div>
</div>
</div>
<!-- Bulk Delete Assets Confirm Modal -->
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="bulkDeleteAssetsConfirmModal" style="z-index: 130;">
<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">Xóa tài sản đã chọn</h3>
</div>
<div class="p-6">
<p id="bulkDeleteAssetsConfirmMessage" class="text-sm text-slate-600 mb-6">Bạn có chắc muốn xóa các tài sản đã chọn?</p>
<div class="mb-4 rounded-lg border border-red-100 bg-red-50 px-3 py-2 text-xs text-red-700">
Số lượng: <strong id="bulkDeleteAssetsConfirmCount">0</strong> tài sản
</div>
<div class="flex gap-3">
<button type="button" class="cancel-bulk-asset-delete-confirm flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeBulkDeleteAssetsConfirmModal()">Hủy</button>
<button type="button" id="confirmBulkAssetDeleteBtn" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold">Xóa</button>
</div>
</div>
</div>
</div>
<!-- Add/Edit Asset Department Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetDepartmentModal">
<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 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900" id="assetDepartmentModalTitle">Thêm phòng ban</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetDepartmentModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="assetDepartmentForm" class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên phòng ban</label>
<input type="text" id="assetDepartmentNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Ví dụ: Kỹ thuật">
</div>
<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="closeAssetDepartmentModal()">Hủy</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">Lưu</button>
</div>
</form>
</div>
</div>
<!-- Delete Asset Department Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteAssetDepartmentModal">
<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">Xóa phòng ban</h3>
</div>
<div class="p-6">
<p class="text-sm text-slate-600 mb-6">Bạn có chắc muốn xóa phòng ban <strong id="deleteAssetDepartmentName">-</strong>? Các tài sản đang gắn phòng ban này sẽ được để trống phòng ban.</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="closeDeleteAssetDepartmentModal()">Hủy</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-asset-department">Xóa</button>
</div>
</div>
</div>
</div>
<!-- Add/Edit Asset Project Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetProjectModal">
<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 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900" id="assetProjectModalTitle">Thêm dự án</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetProjectModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="assetProjectForm" class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên dự án</label>
<input type="text" id="assetProjectNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Ví dụ: AGV">
</div>
<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="closeAssetProjectModal()">Hủy</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">Lưu</button>
</div>
</form>
</div>
</div>
<!-- Delete Asset Project Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteAssetProjectModal">
<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">Xóa dự án</h3>
</div>
<div class="p-6">
<p class="text-sm text-slate-600 mb-6">Bạn có chắc muốn xóa dự án <strong id="deleteAssetProjectName">-</strong>? Các tài sản đang gắn dự án này sẽ được để trống dự án.</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="closeDeleteAssetProjectModal()">Hủy</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-asset-project">Xóa</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Robot Manager Account - Accounts Management</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="../css/main.css" />
<!-- Notiflix Notify -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/notiflix@3.2.7/dist/notiflix-3.2.7.min.css" />
<script src="https://cdn.jsdelivr.net/npm/notiflix@3.2.7/dist/notiflix-aio-3.2.7.min.js"></script>
<style>
.material-symbols-outlined { font-family: 'Material Symbols Outlined'; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20; font-size: 1.25rem; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-flex; white-space: nowrap; word-wrap: normal; direction: ltr; }
body { font-family: 'Inter', sans-serif; height: 100vh; overflow: hidden; }
h1, h2, h3, .brand-logo { font-family: 'Manrope', sans-serif; }
.modal-backdrop { opacity: 0; transition: opacity 0.2s ease-in-out; pointer-events: none; }
.modal-backdrop.open { opacity: 1; pointer-events: auto; }
.modal-content { transform: scale(0.95); transition: transform 0.2s ease-in-out; }
.modal-backdrop.open .modal-content { transform: scale(1); }
</style>
</head>
<body>
<script>
window.location.href = './index.html#accounts';
</script>
</body>
</html>

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Robot Manager Account - Applications Management</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="../css/main.css" />
<!-- Notiflix Notify -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/notiflix@3.2.7/dist/notiflix-3.2.7.min.css" />
<script src="https://cdn.jsdelivr.net/npm/notiflix@3.2.7/dist/notiflix-aio-3.2.7.min.js"></script>
<style>
.material-symbols-outlined { font-family: 'Material Symbols Outlined'; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20; font-size: 1.25rem; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-flex; white-space: nowrap; word-wrap: normal; direction: ltr; }
body { font-family: 'Inter', sans-serif; height: 100vh; overflow: hidden; }
h1, h2, h3, .brand-logo { font-family: 'Manrope', sans-serif; }
.modal-backdrop { opacity: 0; transition: opacity 0.2s ease-in-out; pointer-events: none; }
.modal-backdrop.open { opacity: 1; pointer-events: auto; }
.modal-content { transform: scale(0.95); transition: transform 0.2s ease-in-out; }
.modal-backdrop.open .modal-content { transform: scale(1); }
</style>
</head>
<body>
<script>
window.location.href = './index.html#applications';
</script>
</body>
</html>

310
public/pages/index.html Normal file
View File

@@ -0,0 +1,310 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Robot Manager Account - Hệ thống quản lý</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="../css/main.css" />
<!-- Notiflix Notify -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/notiflix@3.2.7/dist/notiflix-3.2.7.min.css" />
<script src="https://cdn.jsdelivr.net/npm/notiflix@3.2.7/dist/notiflix-aio-3.2.7.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<style>
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
font-size: 1.25rem;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-flex;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}
body { font-family: 'Inter', sans-serif; height: 100vh; overflow: hidden; }
h1, h2, h3, .brand-logo { font-family: 'Manrope', sans-serif; }
.modal-backdrop {
opacity: 0;
transition: opacity 0.2s ease-in-out;
pointer-events: none;
}
.modal-backdrop.open {
opacity: 1;
pointer-events: auto;
}
.modal-content {
transform: scale(0.95);
transition: transform 0.2s ease-in-out;
}
.modal-backdrop.open .modal-content {
transform: scale(1);
}
.tree-label {
letter-spacing: 0.08em;
text-transform: uppercase;
font-size: 10px;
font-weight: 800;
color: #64748b;
padding: 0 0.75rem;
margin-bottom: 0.35rem;
}
.tree-branch {
margin-left: 0.75rem;
border-left: 1px dashed rgba(100, 116, 139, 0.5);
padding-left: 0.6rem;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
body.app-shell {
overflow: hidden;
}
#mobileMenuBtn,
#sidebarBackdrop {
display: none;
}
@media (max-width: 900px) {
body.app-shell {
width: 100%;
min-height: 100dvh;
height: 100dvh;
overflow: hidden;
position: relative;
}
#mobileMenuBtn {
display: inline-flex;
align-items: center;
justify-content: center;
}
#sidebarBackdrop {
display: block;
position: fixed;
inset: 0;
z-index: 70;
background: rgba(15, 23, 42, 0.45);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease-in-out;
}
#appSidebar {
position: fixed;
inset: 0 auto 0 0;
height: 100dvh;
width: min(82vw, 16rem);
z-index: 80;
transform: translateX(-100%);
transition: transform 0.2s ease-in-out;
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.35);
}
body.mobile-nav-open #sidebarBackdrop {
opacity: 1;
pointer-events: auto;
}
body.mobile-nav-open #appSidebar {
transform: translateX(0);
}
#appMain {
width: 100%;
min-width: 0;
height: 100dvh;
}
#appMain > header {
padding-left: 0.75rem;
padding-right: 0.75rem;
gap: 0.5rem;
}
.topbar-actions {
gap: 0.5rem;
}
#profileBtn {
padding: 0.4rem 0.55rem;
}
#profileBtn .profile-meta {
display: none;
}
.dashboard-header,
.page-header,
.page-filters,
.users-controls {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.page-filters > div,
.users-controls > div,
.users-controls select {
width: 100%;
}
.dashboard-actions,
.page-header button,
.page-header > button {
width: 100%;
}
.dashboard-stats,
.apps-stats {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.table-wrap {
overflow-x: auto;
}
.table-wrap table {
min-width: 700px;
}
.page-pager {
gap: 0.5rem;
flex-direction: column;
align-items: flex-start;
}
.modal-backdrop {
align-items: flex-end;
}
.modal-backdrop .modal-content {
width: calc(100% - 1rem);
max-height: min(88dvh, 700px);
margin: 0.5rem;
overflow-y: auto;
border-radius: 0.9rem;
}
}
@media (max-width: 560px) {
.dashboard-stats,
.apps-stats {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body class="app-shell bg-background text-on-surface antialiased flex h-screen w-screen">
<!-- SideNavBar -->
<aside id="appSidebar" class="h-screen w-56 flex flex-col bg-slate-100 dark:bg-slate-900 font-manrope text-sm font-medium border-r border-outline-variant/10 shrink-0">
<div class="flex flex-col h-full py-6">
<!-- Header -->
<div class="px-6 mb-8">
<div class="text-lg font-black text-slate-900 dark:text-slate-50 tracking-tight leading-none">Robot Account</div>
<div class="text-[10px] uppercase tracking-widest text-on-surface-variant mt-1.5 font-bold">Admin Console</div>
</div>
<!-- Primary Nav -->
<nav class="flex-1 px-3 space-y-4">
<div>
<div class="tree-label">Tài khoản</div>
<div class="tree-branch">
<a href="#dashboard" data-nav="dashboard" class="flex items-center gap-3 px-3 py-2 border-l-4 border-blue-600 bg-slate-200/80 dark:bg-slate-800 text-slate-900 dark:text-slate-50 font-bold group transition-all cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">dashboard</span>
<span>Tổng quan</span>
</a>
<a href="#applications" data-nav="applications" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">apps</span>
<span>Ứng dụng</span>
</a>
<a href="#accounts" data-nav="accounts" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">manage_accounts</span>
<span>Tài khoản</span>
</a>
</div>
</div>
<div>
<div class="tree-label">Quản lý tài sản</div>
<div class="tree-branch">
<a href="#assets" data-nav="assets" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">inventory_2</span>
<span>Tài sản</span>
</a>
<a href="#asset-borrows" data-nav="asset-borrows" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">assignment_returned</span>
<span>Mượn/Trả tài sản</span>
</a>
<a href="#asset-departments" data-nav="asset-departments" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">apartment</span>
<span> Phòng Ban</span>
</a>
<a href="#asset-projects" data-nav="asset-projects" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">workspaces</span>
<span>Dự án</span>
</a>
</div>
</div>
<div id="usersSection" class="pt-2 border-t border-outline-variant/10" style="display: none;">
<a id="usersNav" href="#users" data-nav="users" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-lg" style="display: none;">
<span class="material-symbols-outlined">people</span>
<span>Người dùng (Quản trị)</span>
</a>
</div>
</nav>
<!-- Footer -->
<div class="px-6 pt-4 border-t border-outline-variant/10">
<div class="text-[10px] font-bold text-on-surface-variant/40 uppercase tracking-widest">v1.0.0</div>
</div>
</div>
</aside>
<button id="sidebarBackdrop" type="button" aria-label="Đóng menu điều hướng"></button>
<!-- Main Content -->
<main id="appMain" class="flex-1 flex flex-col h-screen min-w-0">
<!-- TopAppBar -->
<header class="h-14 flex items-center justify-between px-6 bg-slate-50/80 dark:bg-slate-950/80 backdrop-blur-xl border-b border-outline-variant/10 shrink-0">
<div class="flex items-center gap-4 flex-1">
<button id="mobileMenuBtn" type="button" class="p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" aria-label="Mở menu điều hướng" aria-controls="appSidebar" aria-expanded="false">
<span class="material-symbols-outlined">menu</span>
</button>
</div>
<div class="topbar-actions flex items-center gap-4">
<button id="pendingAssetRequestsBtn" type="button" class="hidden relative flex items-center gap-2 px-3 py-2 rounded-lg border border-amber-200 bg-amber-50 hover:bg-amber-100 text-amber-800 transition-colors" title="Đơn chờ xử lý">
<span class="material-symbols-outlined text-base">notifications_active</span>
<span class="text-xs font-bold">Đơn chờ</span>
<span id="pendingAssetRequestsBadge" class="hidden absolute -top-2.5 -right-2.5 min-w-[22px] h-[22px] px-1.5 rounded-[999px] bg-red-600 text-white text-xs font-extrabold leading-[22px] text-center ring-2 ring-white">0</span>
</button>
<button id="profileBtn" type="button" class="profile-btn flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors" title="Sửa hồ sơ">
<span class="material-symbols-outlined text-slate-600 dark:text-slate-400">account_circle</span>
<div class="profile-meta flex flex-col">
<span id="accountUsername" class="text-xs font-semibold text-slate-900 dark:text-slate-50">Tài khoản người dùng</span>
<span id="accountRole" class="text-[10px] text-slate-500 dark:text-slate-400">Quản trị viên</span>
</div>
</button>
<button id="logoutBtn" class="p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-red-100 dark:hover:bg-red-950 hover:text-red-700 dark:hover:text-red-300 transition-colors" title="Đăng xuất">
<span class="material-symbols-outlined">logout</span>
</button>
</div>
</header>
<!-- Main Content Area -->
<div id="mainContent" class="flex-1 overflow-hidden">
<!-- Content will be rendered here by JavaScript -->
</div>
</main>
<script src="../js/app.js?v=20260424-1"></script>
</body>
</html>

710
public/pages/login.html Normal file
View File

@@ -0,0 +1,710 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Robotics Account - Login</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="../css/main.css" />
<style>
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
font-size: 1.25rem;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-flex;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}
body { font-family: 'Inter', sans-serif; min-height: 100vh; }
h1, h2, h3 { font-family: 'Manrope', sans-serif; }
</style>
</head>
<body class="bg-gradient-to-br from-blue-50 via-background to-purple-50 dark:from-slate-950 dark:via-slate-900 dark:to-slate-950 antialiased">
<div class="min-h-screen flex items-center justify-center px-4">
<div class="w-full max-w-md">
<!-- Login Card -->
<div class="bg-white dark:bg-slate-900 rounded-2xl shadow-lg border border-outline-variant/10 p-8">
<!-- Header -->
<div class="text-center mb-8">
<div class="flex justify-center mb-4">
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center">
<span class="material-symbols-outlined text-primary text-4xl">security</span>
</div>
</div>
<h1 class="text-2xl font-black text-slate-900 dark:text-slate-50 tracking-tight">Robotics Account Manager</h1>
<p class="text-xs uppercase tracking-widest text-on-surface-variant font-bold mt-2">Account Management System</p>
</div>
<div id="authTabs" class="flex gap-2 mb-6" role="tablist" aria-label="Auth switcher">
<button id="loginTab" type="button" class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-primary text-on-primary shadow-sm">Đăng nhập</button>
<button id="registerTab" type="button" class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-surface-container-low text-on-surface-variant">Đăng ký</button>
</div>
<!-- Login Form -->
<form id="loginForm" class="space-y-5">
<!-- Username/Email Input -->
<div>
<label for="username" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Username or Email</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">person</span>
</span>
<input
type="text"
id="username"
name="username"
placeholder="Enter your username"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<!-- Password Input -->
<div>
<label for="password" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Password</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">lock</span>
</span>
<input
type="password"
id="password"
name="password"
placeholder="Enter your password"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<!-- Remember Me Checkbox -->
<div class="flex items-center justify-between gap-2">
<div class="flex items-center">
<input
type="checkbox"
id="remember"
name="remember"
class="w-4 h-4 rounded border-outline-variant/30 text-primary focus:ring-2 focus:ring-primary/50 cursor-pointer"
/>
<label for="remember" class="ml-2.5 text-xs font-medium text-on-surface-variant cursor-pointer">Remember me</label>
</div>
<button id="forgotPasswordLink" type="button" class="text-xs font-semibold text-primary hover:text-primary-dim transition-colors bg-transparent border-0 p-0">
Forgot password?
</button>
</div>
<!-- Login Button -->
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-6"
>
<span class="material-symbols-outlined text-sm">login</span>
<span>Sign In</span>
</button>
<!-- Error Message -->
<div id="errorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
<div id="verifyNotice" class="hidden bg-amber-50 text-amber-800 border border-amber-200 rounded-lg px-4 py-3 text-xs font-medium space-y-2">
<p id="verifyNoticeText">Please confirm your email before signing in.</p>
<button id="resendVerifyBtn" type="button" class="inline-flex items-center gap-1 px-3 py-1.5 rounded-md bg-amber-100 hover:bg-amber-200 text-amber-900 font-semibold transition-colors">
<span class="material-symbols-outlined text-sm">forward_to_inbox</span>
<span>Resend confirmation email</span>
</button>
</div>
</form>
<form id="forgotPasswordForm" class="space-y-5 hidden">
<div class="bg-blue-50 border border-blue-100 rounded-lg px-4 py-3 text-xs text-blue-700">
Enter your username and registered email to receive a password reset link.
</div>
<div>
<label for="forgotUsername" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Username</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">person</span>
</span>
<input
type="text"
id="forgotUsername"
name="forgotUsername"
placeholder="Enter your username"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="forgotEmail" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Registered Email</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">mail</span>
</span>
<input
type="email"
id="forgotEmail"
name="forgotEmail"
placeholder="Enter your registered email"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-4"
>
<span class="material-symbols-outlined text-sm">mark_email_unread</span>
<span>Send reset email</span>
</button>
<button id="forgotBackToLoginBtn" type="button" class="w-full bg-surface-container-low hover:bg-slate-200 text-on-surface-variant font-semibold py-2.5 px-4 rounded-lg transition-colors">
Back to sign in
</button>
<div id="forgotPasswordErrorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
<div id="forgotPasswordSuccessMessage" class="hidden bg-green-50 text-green-800 border border-green-200 rounded-lg px-4 py-3 text-xs font-medium"></div>
</form>
<form id="resetPasswordForm" class="space-y-5 hidden">
<div class="bg-blue-50 border border-blue-100 rounded-lg px-4 py-3 text-xs text-blue-700">
Set your new password below.
</div>
<div>
<label for="resetPassword" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">New Password</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">lock_reset</span>
</span>
<input
type="password"
id="resetPassword"
name="resetPassword"
placeholder="Enter a new password"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="resetConfirmPassword" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Confirm New Password</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">verified_user</span>
</span>
<input
type="password"
id="resetConfirmPassword"
name="resetConfirmPassword"
placeholder="Re-enter new password"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-4"
>
<span class="material-symbols-outlined text-sm">password</span>
<span>Reset password</span>
</button>
<button id="resetBackToLoginBtn" type="button" class="w-full bg-surface-container-low hover:bg-slate-200 text-on-surface-variant font-semibold py-2.5 px-4 rounded-lg transition-colors">
Back to sign in
</button>
<div id="resetPasswordErrorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
</form>
<!-- Register Form -->
<form id="registerForm" class="space-y-5 hidden">
<div>
<label for="regFullname" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Full Name</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">badge</span>
</span>
<input
type="text"
id="regFullname"
name="fullname"
placeholder="Enter your full name"
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="regEmail" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Email</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">mail</span>
</span>
<input
type="email"
id="regEmail"
name="email"
placeholder="Enter your email"
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="regUsername" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Username</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">person_add</span>
</span>
<input
type="text"
id="regUsername"
name="username"
placeholder="Choose a username"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="regPassword" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Password</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">lock</span>
</span>
<input
type="password"
id="regPassword"
name="password"
placeholder="Create a password"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-4"
>
<span class="material-symbols-outlined text-sm">how_to_reg</span>
<span>Tạo tài khoản</span>
</button>
<div id="registerErrorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
<div id="registerSuccessMessage" class="hidden bg-green-50 text-green-800 border border-green-200 rounded-lg px-4 py-3 text-xs font-medium"></div>
</form>
<!-- Footer -->
<!-- <div class="mt-8 pt-6 border-t border-outline-variant/10 text-center">
<p class="text-[10px] text-on-surface-variant/60">Default credentials for demo: admin / admin</p>
</div> -->
</div>
<!-- Bottom info -->
<div class="text-center mt-6">
<p class="text-xs text-on-surface-variant/60">v1.0.0</p>
</div>
</div>
</div>
<script>
const authTabs = document.getElementById('authTabs');
const loginForm = document.getElementById('loginForm');
const errorMessage = document.getElementById('errorMessage');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const rememberCheckbox = document.getElementById('remember');
const forgotPasswordLink = document.getElementById('forgotPasswordLink');
const forgotPasswordForm = document.getElementById('forgotPasswordForm');
const forgotUsernameInput = document.getElementById('forgotUsername');
const forgotEmailInput = document.getElementById('forgotEmail');
const forgotBackToLoginBtn = document.getElementById('forgotBackToLoginBtn');
const forgotPasswordErrorMessage = document.getElementById('forgotPasswordErrorMessage');
const forgotPasswordSuccessMessage = document.getElementById('forgotPasswordSuccessMessage');
const resetPasswordForm = document.getElementById('resetPasswordForm');
const resetPasswordInput = document.getElementById('resetPassword');
const resetConfirmPasswordInput = document.getElementById('resetConfirmPassword');
const resetBackToLoginBtn = document.getElementById('resetBackToLoginBtn');
const resetPasswordErrorMessage = document.getElementById('resetPasswordErrorMessage');
const registerForm = document.getElementById('registerForm');
const registerErrorMessage = document.getElementById('registerErrorMessage');
const registerSuccessMessage = document.getElementById('registerSuccessMessage');
const verifyNotice = document.getElementById('verifyNotice');
const verifyNoticeText = document.getElementById('verifyNoticeText');
const resendVerifyBtn = document.getElementById('resendVerifyBtn');
const regFullnameInput = document.getElementById('regFullname');
const regEmailInput = document.getElementById('regEmail');
const regUsernameInput = document.getElementById('regUsername');
const regPasswordInput = document.getElementById('regPassword');
const loginTab = document.getElementById('loginTab');
const registerTab = document.getElementById('registerTab');
let pendingVerificationIdentifier = '';
let currentMode = 'login';
let resetToken = '';
const setMode = (mode) => {
currentMode = mode;
const isLogin = mode === 'login';
const isRegister = mode === 'register';
const isForgot = mode === 'forgot';
const isReset = mode === 'reset';
loginForm.classList.toggle('hidden', !isLogin);
registerForm.classList.toggle('hidden', !isRegister);
forgotPasswordForm.classList.toggle('hidden', !isForgot);
resetPasswordForm.classList.toggle('hidden', !isReset);
authTabs.classList.toggle('hidden', isReset);
errorMessage.classList.add('hidden');
registerErrorMessage.classList.add('hidden');
registerSuccessMessage.classList.add('hidden');
verifyNotice.classList.add('hidden');
forgotPasswordErrorMessage.classList.add('hidden');
forgotPasswordSuccessMessage.classList.add('hidden');
resetPasswordErrorMessage.classList.add('hidden');
const activate = (btn, active) => {
btn.classList.toggle('bg-primary', active);
btn.classList.toggle('text-on-primary', active);
btn.classList.toggle('shadow-sm', active);
btn.classList.toggle('bg-surface-container-low', !active);
btn.classList.toggle('text-on-surface-variant', !active);
};
activate(loginTab, !isRegister);
activate(registerTab, isRegister);
};
const clearResetQuery = () => {
const cleanUrl = `${window.location.origin}${window.location.pathname}`;
window.history.replaceState({}, document.title, cleanUrl);
};
const getInitialMode = () => {
const params = new URLSearchParams(window.location.search);
const mode = String(params.get('mode') || '').trim().toLowerCase();
const token = String(params.get('token') || '').trim();
if (mode === 'reset-password') {
resetToken = token;
return token ? 'reset' : 'forgot';
}
if (mode === 'forgot-password') {
return 'forgot';
}
return 'login';
};
document.addEventListener('DOMContentLoaded', () => {
const initialMode = getInitialMode();
const currentUser = localStorage.getItem('currentUser');
if (currentUser && initialMode !== 'reset') {
window.location.href = './index.html';
return;
}
setMode(initialMode);
if (initialMode === 'forgot') {
forgotUsernameInput.value = usernameInput.value.trim();
}
if (initialMode === 'reset' && !resetToken) {
setMode('forgot');
forgotPasswordErrorMessage.textContent = 'Reset link is invalid. Please request a new password reset email.';
forgotPasswordErrorMessage.classList.remove('hidden');
}
const rememberedUsername = localStorage.getItem('rememberedUsername');
if (rememberedUsername) {
usernameInput.value = rememberedUsername;
rememberCheckbox.checked = true;
if (!forgotUsernameInput.value) {
forgotUsernameInput.value = rememberedUsername;
}
}
});
loginTab.addEventListener('click', () => {
clearResetQuery();
setMode('login');
});
registerTab.addEventListener('click', () => {
clearResetQuery();
setMode('register');
});
forgotPasswordLink.addEventListener('click', () => {
setMode('forgot');
forgotUsernameInput.value = usernameInput.value.trim();
forgotEmailInput.focus();
});
forgotBackToLoginBtn.addEventListener('click', () => {
clearResetQuery();
setMode('login');
});
resetBackToLoginBtn.addEventListener('click', () => {
resetToken = '';
clearResetQuery();
setMode('login');
});
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
errorMessage.classList.add('hidden');
verifyNotice.classList.add('hidden');
const username = usernameInput.value.trim();
const password = passwordInput.value;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success && data.user) {
localStorage.setItem('currentUser', JSON.stringify(data.user));
if (rememberCheckbox.checked) {
localStorage.setItem('rememberedUsername', username);
} else {
localStorage.removeItem('rememberedUsername');
}
window.location.href = './index.html';
} else if (data.requiresEmailVerification) {
pendingVerificationIdentifier = data.username || data.email || username;
verifyNoticeText.textContent = data.message || 'Please confirm your email before signing in';
verifyNotice.classList.remove('hidden');
} else {
errorMessage.textContent = data.message || 'Invalid username or password';
errorMessage.classList.remove('hidden');
passwordInput.value = '';
passwordInput.focus();
}
} catch (error) {
errorMessage.textContent = 'Connection error. Try again later.';
errorMessage.classList.remove('hidden');
console.error('Login error:', error);
}
});
forgotPasswordForm.addEventListener('submit', async (e) => {
e.preventDefault();
forgotPasswordErrorMessage.classList.add('hidden');
forgotPasswordSuccessMessage.classList.add('hidden');
const payload = {
username: forgotUsernameInput.value.trim(),
email: forgotEmailInput.value.trim()
};
if (!payload.username || !payload.email) {
forgotPasswordErrorMessage.textContent = 'Username and email are required.';
forgotPasswordErrorMessage.classList.remove('hidden');
return;
}
try {
const response = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok && data?.success) {
const lines = [data.message || 'If the account exists, a password reset email has been sent.'];
if (data.resetPreviewUrl) {
lines.push(`Development reset link: ${data.resetPreviewUrl}`);
}
forgotPasswordSuccessMessage.textContent = lines.join(' ');
forgotPasswordSuccessMessage.classList.remove('hidden');
} else {
forgotPasswordErrorMessage.textContent = data?.message || 'Forgot password request failed.';
forgotPasswordErrorMessage.classList.remove('hidden');
}
} catch (error) {
forgotPasswordErrorMessage.textContent = 'Connection error. Please try again.';
forgotPasswordErrorMessage.classList.remove('hidden');
console.error('Forgot password error:', error);
}
});
resetPasswordForm.addEventListener('submit', async (e) => {
e.preventDefault();
resetPasswordErrorMessage.classList.add('hidden');
const newPassword = resetPasswordInput.value;
const confirmPassword = resetConfirmPasswordInput.value;
if (!resetToken) {
resetPasswordErrorMessage.textContent = 'Reset link is invalid. Please request a new one.';
resetPasswordErrorMessage.classList.remove('hidden');
return;
}
if (!newPassword || newPassword.length < 6) {
resetPasswordErrorMessage.textContent = 'New password must be at least 6 characters.';
resetPasswordErrorMessage.classList.remove('hidden');
return;
}
if (newPassword !== confirmPassword) {
resetPasswordErrorMessage.textContent = 'Confirm password does not match.';
resetPasswordErrorMessage.classList.remove('hidden');
return;
}
try {
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: resetToken,
newPassword
})
});
const data = await response.json();
if (response.ok && data?.success) {
resetToken = '';
resetPasswordInput.value = '';
resetConfirmPasswordInput.value = '';
clearResetQuery();
setMode('login');
verifyNoticeText.textContent = data.message || 'Password reset successful. Please sign in.';
verifyNotice.classList.remove('hidden');
passwordInput.focus();
} else {
resetPasswordErrorMessage.textContent = data?.message || 'Password reset failed.';
resetPasswordErrorMessage.classList.remove('hidden');
}
} catch (error) {
resetPasswordErrorMessage.textContent = 'Connection error. Please try again.';
resetPasswordErrorMessage.classList.remove('hidden');
console.error('Reset password error:', error);
}
});
registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
registerErrorMessage.classList.add('hidden');
registerSuccessMessage.classList.add('hidden');
const payload = {
fullname: regFullnameInput.value.trim(),
email: regEmailInput.value.trim(),
username: regUsernameInput.value.trim(),
password: regPasswordInput.value
};
if (!payload.username || !payload.password || !payload.email) {
registerErrorMessage.textContent = 'Username, password and email are required';
registerErrorMessage.classList.remove('hidden');
return;
}
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const isJson = response.headers.get('content-type')?.includes('application/json');
const data = isJson ? await response.json() : null;
if (response.ok && data?.success) {
const lines = [data.message || 'Registration successful. Please check your email to confirm account.'];
if (data.verificationPreviewUrl) {
lines.push(`Development verification link: ${data.verificationPreviewUrl}`);
}
regPasswordInput.value = '';
setMode('login');
usernameInput.value = payload.username;
passwordInput.value = '';
pendingVerificationIdentifier = payload.email || payload.username;
verifyNoticeText.textContent = lines.join(' ');
verifyNotice.classList.remove('hidden');
} else {
const fallback = isJson ? (data?.message || 'Registration failed') : 'Server error (HTML response)';
registerErrorMessage.textContent = fallback;
registerErrorMessage.classList.remove('hidden');
}
} catch (error) {
registerErrorMessage.textContent = 'Connection error. Please try again.';
registerErrorMessage.classList.remove('hidden');
console.error('Register error:', error);
}
});
resendVerifyBtn.addEventListener('click', async () => {
const identifier = pendingVerificationIdentifier || usernameInput.value.trim();
if (!identifier) {
verifyNoticeText.textContent = 'Enter username/email first, then try resending verification.';
verifyNotice.classList.remove('hidden');
return;
}
try {
const response = await fetch('/api/auth/resend-verification', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ identifier })
});
const data = await response.json();
verifyNoticeText.textContent = data.message || 'Verification email processed.';
verifyNotice.classList.remove('hidden');
if (data.verificationPreviewUrl) {
verifyNoticeText.textContent += ` Development link: ${data.verificationPreviewUrl}`;
}
} catch (error) {
verifyNoticeText.textContent = 'Cannot resend verification email right now.';
verifyNotice.classList.remove('hidden');
console.error('Resend verification error:', error);
}
});
</script>
</body>
</html>

46
public/pages/users.html Normal file
View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Robot Manager Account - Users Management</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="../css/main.css" />
<!-- Notiflix Notify -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/notiflix@3.2.7/dist/notiflix-3.2.7.min.css" />
<script src="https://cdn.jsdelivr.net/npm/notiflix@3.2.7/dist/notiflix-aio-3.2.7.min.js"></script>
<style>
.material-symbols-outlined { font-family: 'Material Symbols Outlined'; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20; font-size: 1.25rem; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-flex; white-space: nowrap; word-wrap: normal; direction: ltr; }
body { font-family: 'Inter', sans-serif; height: 100vh; overflow: hidden; }
h1, h2, h3, .brand-logo { font-family: 'Manrope', sans-serif; }
.modal-backdrop { opacity: 0; transition: opacity 0.2s ease-in-out; pointer-events: none; }
.modal-backdrop.open { opacity: 1; pointer-events: auto; }
.modal-content { transform: scale(0.95); transition: transform 0.2s ease-in-out; }
.modal-backdrop.open .modal-content { transform: scale(1); }
</style>
</head>
<body>
<script>
// Check if user has admin role, if not redirect to dashboard
const currentUser = localStorage.getItem('currentUser');
if (currentUser) {
try {
const user = JSON.parse(currentUser);
if (user.Role !== 'admin') {
window.location.href = './dashboard.html';
}
} catch (e) {
window.location.href = './login.html';
}
} else {
window.location.href = './login.html';
}
</script>
<script>
window.location.href = './index.html#users';
</script>
</body>
</html>

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en" class="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Confirm Email - AccManager</title>
<link rel="stylesheet" href="../css/main.css" />
<style>
body { font-family: 'Inter', sans-serif; min-height: 100vh; }
</style>
</head>
<body class="bg-gradient-to-br from-slate-100 via-white to-blue-100 text-slate-900 antialiased">
<main class="min-h-screen flex items-center justify-center px-4">
<section class="w-full max-w-md bg-white rounded-2xl border border-slate-200 shadow-lg p-8">
<h1 class="text-2xl font-black tracking-tight mb-2">Email Confirmation</h1>
<p class="text-sm text-slate-600 mb-6">We are confirming your account.</p>
<div id="statusBox" class="rounded-lg border px-4 py-3 text-sm bg-slate-50 border-slate-200 text-slate-700">
Confirming your email, please wait...
</div>
<div class="mt-6 flex gap-3">
<a href="./login.html" class="flex-1 text-center px-4 py-2 rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-700 font-semibold transition-colors">Back to Login</a>
<a id="signInBtn" href="./login.html" class="hidden flex-1 text-center px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold transition-colors">Sign In</a>
</div>
</section>
</main>
<script>
(async () => {
const statusBox = document.getElementById('statusBox');
const signInBtn = document.getElementById('signInBtn');
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
if (!token) {
statusBox.className = 'rounded-lg border px-4 py-3 text-sm bg-red-50 border-red-200 text-red-700';
statusBox.textContent = 'Missing verification token.';
return;
}
try {
const response = await fetch(`/api/auth/verify-email?token=${encodeURIComponent(token)}`);
const data = await response.json();
if (response.ok && data.success) {
statusBox.className = 'rounded-lg border px-4 py-3 text-sm bg-green-50 border-green-200 text-green-700';
if (data.autoLogin && data.user) {
localStorage.setItem('currentUser', JSON.stringify(data.user));
statusBox.textContent = data.message || 'Email confirmed. Redirecting to dashboard...';
setTimeout(() => {
window.location.href = './index.html';
}, 1000);
} else {
statusBox.textContent = data.message || 'Email confirmed successfully.';
signInBtn.classList.remove('hidden');
}
} else {
statusBox.className = 'rounded-lg border px-4 py-3 text-sm bg-red-50 border-red-200 text-red-700';
statusBox.textContent = data.message || 'Email confirmation failed.';
}
} catch (error) {
statusBox.className = 'rounded-lg border px-4 py-3 text-sm bg-red-50 border-red-200 text-red-700';
statusBox.textContent = 'Cannot verify email right now. Please try again later.';
console.error('Verify email page error:', error);
}
})();
</script>
</body>
</html>

View File

@@ -1,4 +0,0 @@
Flask==2.3.2
Flask-CORS==4.0.0
pyodbc==4.0.35
python-dotenv==1.0.0

548
server.js
View File

@@ -1,548 +0,0 @@
// Backend Server for AccManager
// Express.js + mssql
const express = require('express');
const sql = require('mssql');
const cors = require('cors');
const app = express();
// Middleware
app.use(cors());
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
const sqlConfig = {
server: '172.20.235.176',
authentication: {
type: 'default',
options: {
userName: 'sa',
password: 'robotics@2022'
}
},
options: {
database: 'AccManager',
trustServerCertificate: true,
enableKeepAlive: true,
connectTimeout: 30000,
encrypt: false
}
};
// Initialize Database Pool
let pool;
async function initializeDatabase() {
try {
pool = new sql.ConnectionPool(sqlConfig);
await pool.connect();
console.log('✓ Connected to SQL Server');
// Check and create database if not exists
const masterConnection = new sql.ConnectionPool({
server: '172.20.235.176',
authentication: { type: 'default', options: { userName: 'sa', password: 'robotics@2022' } },
options: { connectTimeout: 30000, database: 'master', trustServerCertificate: true, encrypt: false }
});
await masterConnection.connect();
const createDbResult = await masterConnection.request()
.query(`IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'AccManager')
BEGIN
CREATE DATABASE AccManager;
END`);
await masterConnection.close();
// Now create tables in AccManager
await createTables();
console.log('✓ Database and tables created');
} catch (err) {
console.error('Database connection failed:', err);
process.exit(1);
}
}
async function createTables() {
const queries = [
// Users Table
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Users')
BEGIN
CREATE TABLE Users (
UserId INT PRIMARY KEY IDENTITY(1,1),
Username NVARCHAR(50) UNIQUE NOT NULL,
Password NVARCHAR(255) NOT NULL,
Email NVARCHAR(100),
FullName NVARCHAR(100),
Role NVARCHAR(50) NOT NULL,
Status NVARCHAR(20) DEFAULT 'Active',
CreatedDate DATETIME DEFAULT GETDATE(),
LastLogin DATETIME,
IsActive BIT DEFAULT 1
)
END`,
// Applications Table
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Applications')
BEGIN
CREATE TABLE Applications (
AppId INT PRIMARY KEY IDENTITY(1,1),
Name NVARCHAR(100) NOT NULL,
Type NVARCHAR(50),
Status NVARCHAR(20) DEFAULT 'online',
Icon NVARCHAR(50),
Description NVARCHAR(500),
Url NVARCHAR(255),
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
)
END`,
// Accounts Table
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Accounts')
BEGIN
CREATE TABLE Accounts (
AccountId INT PRIMARY KEY IDENTITY(1,1),
UserId INT NOT NULL,
AppId INT NOT NULL,
AccountUsername NVARCHAR(100),
AccountPassword NVARCHAR(255),
Email NVARCHAR(100),
AccessLevel NVARCHAR(50),
Status NVARCHAR(20) DEFAULT 'Active',
Notes NVARCHAR(MAX),
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
FOREIGN KEY (UserId) REFERENCES Users(UserId) ON DELETE CASCADE,
FOREIGN KEY (AppId) REFERENCES Applications(AppId) ON DELETE CASCADE
)
END`,
// AuditLog Table
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
BEGIN
CREATE TABLE AuditLog (
LogId INT PRIMARY KEY IDENTITY(1,1),
UserId INT,
Action NVARCHAR(50),
TableName NVARCHAR(50),
RecordId INT,
OldValue NVARCHAR(MAX),
NewValue NVARCHAR(MAX),
Timestamp DATETIME DEFAULT GETDATE(),
FOREIGN KEY (UserId) REFERENCES Users(UserId)
)
END`
];
for (let query of queries) {
try {
await pool.request().query(query);
} catch (err) {
console.error('Table creation error:', err.message);
}
}
// 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
try {
await pool.request()
.input('username', sql.NVarChar, 'admin')
.input('password', sql.NVarChar, 'admin')
.input('email', sql.NVarChar, 'admin@accmanager.local')
.input('fullname', sql.NVarChar, 'Administrator')
.input('role', sql.NVarChar, 'admin')
.query(`IF NOT EXISTS (SELECT * FROM Users WHERE Username = @username)
INSERT INTO Users (Username, Password, Email, FullName, Role, IsActive)
VALUES (@username, @password, @email, @fullname, @role, 1)`);
console.log('✓ Admin user created: admin / admin');
} catch (err) {
console.error('Admin user error:', err.message);
}
// Insert sample applications
try {
await pool.request()
.query(`IF (SELECT COUNT(*) FROM Applications) = 0
BEGIN
INSERT INTO Applications (Name, Type, Status, Icon, Description, Url)
VALUES
('AWS', 'Cloud', 'online', 'cloud', 'Amazon Web Services', 'https://aws.amazon.com'),
('GitHub', 'VCS', 'online', 'code', 'GitHub - Version Control', 'https://github.com'),
('Google Workspace', 'Collaboration', 'online', 'mail', 'Google Workspace', 'https://workspace.google.com'),
('Nginx Proxy', 'Infra', 'offline', 'dns', 'Nginx Web Server', 'https://nginx.org')
END`);
console.log('✓ Sample applications created');
} catch (err) {
console.error('Applications error:', err.message);
}
}
// ==========================================
// API ROUTES - Authentication
// ==========================================
// Login endpoint
app.post('/api/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
const result = await pool.request()
.input('username', sql.NVarChar, username)
.input('password', sql.NVarChar, password)
.query('SELECT UserId, Username, Email, FullName, Role, Status FROM Users WHERE Username = @username AND Password = @password AND IsActive = 1');
if (result.recordset.length > 0) {
const user = result.recordset[0];
// Update last login
await pool.request()
.input('userId', sql.Int, user.UserId)
.query('UPDATE Users SET LastLogin = GETDATE() WHERE UserId = @userId');
res.json({
success: true,
message: 'Login successful',
user: user
});
} else {
res.status(401).json({
success: false,
message: 'Invalid username or password'
});
}
} catch (err) {
console.error('Login error:', err);
res.status(500).json({ success: false, message: err.message });
}
});
// ==========================================
// API ROUTES - Users
// ==========================================
// Get all users
app.get('/api/users', async (req, res) => {
try {
const result = await pool.request()
.query('SELECT UserId, Username, Email, FullName, Role, Status, CreatedDate FROM Users ORDER BY CreatedDate DESC');
res.json({ success: true, data: result.recordset });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Get user by ID
app.get('/api/users/:id', async (req, res) => {
try {
const result = await pool.request()
.input('userId', sql.Int, req.params.id)
.query('SELECT * FROM Users WHERE UserId = @userId');
if (result.recordset.length > 0) {
res.json({ success: true, data: result.recordset[0] });
} else {
res.status(404).json({ success: false, message: 'User not found' });
}
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Create new user
app.post('/api/users', async (req, res) => {
try {
const { username, password, email, fullname, role } = req.body;
const result = await pool.request()
.input('username', sql.NVarChar, username)
.input('password', sql.NVarChar, password)
.input('email', sql.NVarChar, email)
.input('fullname', sql.NVarChar, fullname)
.input('role', sql.NVarChar, role)
.query(`INSERT INTO Users (Username, Password, Email, FullName, Role, IsActive)
VALUES (@username, @password, @email, @fullname, @role, 1);
SELECT SCOPE_IDENTITY() as UserId`);
res.json({ success: true, message: 'User created', userId: result.recordset[0].UserId });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ==========================================
// API ROUTES - Applications
// ==========================================
// Get all applications
app.get('/api/applications', async (req, res) => {
try {
const result = await pool.request()
.query('SELECT AppId, Name, Type, Status, Icon, Description, Url, CreatedDate, UpdatedDate FROM Applications ORDER BY Name');
res.json({ success: true, data: result.recordset });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Create application
app.post('/api/applications', async (req, res) => {
try {
const { name, type, status, icon, description, url } = req.body;
const result = await pool.request()
.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(`INSERT INTO Applications (Name, Type, Status, Icon, Description, Url)
VALUES (@name, @type, @status, @icon, @description, @url);
SELECT SCOPE_IDENTITY() as AppId`);
res.json({ success: true, message: 'Application created', appId: result.recordset[0].AppId });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// 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
// ==========================================
// Get accounts for a user
app.get('/api/accounts/user/:userId', async (req, res) => {
try {
const result = await pool.request()
.input('userId', sql.Int, req.params.userId)
.query(`SELECT a.*, app.Name as AppName, app.Type as AppType
FROM Accounts a
JOIN Applications app ON a.AppId = app.AppId
WHERE a.UserId = @userId
ORDER BY a.CreatedDate DESC`);
res.json({ success: true, data: result.recordset });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Create account
app.post('/api/accounts', async (req, res) => {
try {
const { userId, appId, accountUsername, accountPassword, email, accessLevel, notes } = req.body;
const result = await pool.request()
.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(`INSERT INTO Accounts (UserId, AppId, AccountUsername, AccountPassword, Email, AccessLevel, Notes)
VALUES (@userId, @appId, @accountUsername, @accountPassword, @email, @accessLevel, @notes);
SELECT SCOPE_IDENTITY() as AccountId`);
res.json({ success: true, message: 'Account created', accountId: result.recordset[0].AccountId });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// 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
// ==========================================
// Get database information
app.get('/api/database/info', async (req, res) => {
try {
const tables = await pool.request().query(`
SELECT TABLE_NAME as TableName,
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = t.TABLE_NAME) as ColumnCount
FROM INFORMATION_SCHEMA.TABLES t
WHERE TABLE_SCHEMA = 'dbo'
ORDER BY TABLE_NAME
`);
const users = await pool.request().query('SELECT COUNT(*) as Count FROM Users');
const apps = await pool.request().query('SELECT COUNT(*) as Count FROM Applications');
const accounts = await pool.request().query('SELECT COUNT(*) as Count FROM Accounts');
res.json({
success: true,
database: 'AccManager',
server: '172.20.235.176',
tables: tables.recordset,
statistics: {
users: users.recordset[0].Count,
applications: apps.recordset[0].Count,
accounts: accounts.recordset[0].Count
}
});
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'OK', database: 'Connected' });
});
// ==========================================
// Error Handling
// ==========================================
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ success: false, message: err.message });
});
// ==========================================
// Server Startup
// ==========================================
const PORT = process.env.PORT || 3000;
async function startServer() {
try {
await initializeDatabase();
app.listen(PORT, () => {
console.log(`\n========================================`);
console.log(`AccManager Backend Server`);
console.log(`========================================`);
console.log(`✓ Server running on http://localhost:${PORT}`);
console.log(`✓ Database: AccManager`);
console.log(`✓ Default admin: admin / admin`);
console.log(`\nAPI Endpoints:`);
console.log(` POST /api/auth/login`);
console.log(` GET /api/database/info`);
console.log(` GET /api/users`);
console.log(` GET /api/applications`);
console.log(` GET /api/accounts/user/:userId`);
console.log(`========================================\n`);
});
} catch (err) {
console.error('Failed to start server:', err);
process.exit(1);
}
}
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\nShutting down...');
if (pool) {
await pool.close();
}
process.exit(0);
});
startServer();

View File

@@ -1,480 +0,0 @@
# AccManager Backend Server (Python Alternative)
# Requirements: Python 3.8+, Flask, pyodbc
from flask import Flask, request, jsonify, render_template
from flask_cors import CORS
from datetime import datetime
import pyodbc
import json
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
CORS(app)
# SQL Server Connection Settings
DB_SERVER = os.getenv('DB_SERVER', '172.20.235.176')
DB_USER = os.getenv('DB_USER', 'sa')
DB_PASSWORD = os.getenv('DB_PASSWORD', 'robotics@2020')
DB_NAME = os.getenv('DB_NAME', 'AccManager')
# Connection string
CONNECTION_STRING = f'''
Driver={{ODBC Driver 17 for SQL Server}};
Server={DB_SERVER};
Database={DB_NAME};
UID={DB_USER};
PWD={DB_PASSWORD};
TrustServerCertificate=yes;
'''
# Initialize database connection
def get_db_connection():
try:
conn = pyodbc.connect(CONNECTION_STRING)
return conn
except Exception as e:
print(f"Database connection error: {e}")
return None
# Create tables
def create_tables():
conn = get_db_connection()
if not conn:
return False
cursor = conn.cursor()
try:
# Users table
cursor.execute('''
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Users')
BEGIN
CREATE TABLE Users (
UserId INT PRIMARY KEY IDENTITY(1,1),
Username NVARCHAR(50) UNIQUE NOT NULL,
Password NVARCHAR(255) NOT NULL,
Email NVARCHAR(100),
FullName NVARCHAR(100),
Role NVARCHAR(50) NOT NULL,
Status NVARCHAR(20) DEFAULT 'Active',
CreatedDate DATETIME DEFAULT GETDATE(),
LastLogin DATETIME,
IsActive BIT DEFAULT 1
)
END
''')
# Applications table
cursor.execute('''
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Applications')
BEGIN
CREATE TABLE Applications (
AppId INT PRIMARY KEY IDENTITY(1,1),
Name NVARCHAR(100) NOT NULL,
Type NVARCHAR(50),
Status NVARCHAR(20) DEFAULT 'online',
Icon NVARCHAR(50),
Description NVARCHAR(500),
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
)
END
''')
# Accounts table
cursor.execute('''
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Accounts')
BEGIN
CREATE TABLE Accounts (
AccountId INT PRIMARY KEY IDENTITY(1,1),
UserId INT NOT NULL,
AppId INT NOT NULL,
AccountUsername NVARCHAR(100),
AccountPassword NVARCHAR(255),
Email NVARCHAR(100),
AccessLevel NVARCHAR(50),
Status NVARCHAR(20) DEFAULT 'Active',
Notes NVARCHAR(MAX),
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
FOREIGN KEY (UserId) REFERENCES Users(UserId) ON DELETE CASCADE,
FOREIGN KEY (AppId) REFERENCES Applications(AppId) ON DELETE CASCADE
)
END
''')
# AuditLog table
cursor.execute('''
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
BEGIN
CREATE TABLE AuditLog (
LogId INT PRIMARY KEY IDENTITY(1,1),
UserId INT,
Action NVARCHAR(50),
TableName NVARCHAR(50),
RecordId INT,
OldValue NVARCHAR(MAX),
NewValue NVARCHAR(MAX),
Timestamp DATETIME DEFAULT GETDATE(),
FOREIGN KEY (UserId) REFERENCES Users(UserId)
)
END
''')
conn.commit()
# Insert admin user
cursor.execute('''
IF NOT EXISTS (SELECT * FROM Users WHERE Username = 'admin')
INSERT INTO Users (Username, Password, Email, FullName, Role, IsActive)
VALUES ('admin', 'admin', 'admin@accmanager.local', 'Administrator', 'admin', 1)
''')
# Insert sample applications
cursor.execute('''
IF (SELECT COUNT(*) FROM Applications) = 0
BEGIN
INSERT INTO Applications (Name, Type, Status, Icon, Description)
VALUES
('AWS', 'Cloud', 'online', 'cloud', 'Amazon Web Services'),
('GitHub', 'VCS', 'online', 'code', 'GitHub - Version Control'),
('Google Workspace', 'Collaboration', 'online', 'mail', 'Google Workspace'),
('Nginx Proxy', 'Infra', 'offline', 'dns', 'Nginx Web Server')
END
''')
conn.commit()
cursor.close()
conn.close()
return True
except Exception as e:
print(f"Table creation error: {e}")
return False
# ==========================================
# Routes: Health Check
# ==========================================
@app.route('/api/health', methods=['GET'])
def health():
conn = get_db_connection()
if conn:
conn.close()
return jsonify({"status": "OK", "database": "Connected"}), 200
else:
return jsonify({"status": "ERROR", "database": "Disconnected"}), 500
# ==========================================
# Routes: Authentication
# ==========================================
@app.route('/api/auth/login', methods=['POST'])
def login():
try:
data = request.get_json()
username = data.get('username')
password = data.get('password')
conn = get_db_connection()
if not conn:
return jsonify({"success": False, "message": "Database connection failed"}), 500
cursor = conn.cursor()
cursor.execute('''
SELECT UserId, Username, Email, FullName, Role, Status
FROM Users
WHERE Username = ? AND Password = ? AND IsActive = 1
''', (username, password))
user = cursor.fetchone()
if user:
cursor.execute('UPDATE Users SET LastLogin = GETDATE() WHERE UserId = ?', user[0])
conn.commit()
return jsonify({
"success": True,
"message": "Login successful",
"user": {
"UserId": user[0],
"username": user[1],
"email": user[2],
"fullname": user[3],
"role": user[4],
"status": user[5]
}
}), 200
else:
return jsonify({
"success": False,
"message": "Invalid username or password"
}), 401
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
finally:
if conn:
conn.close()
# ==========================================
# Routes: Users
# ==========================================
@app.route('/api/users', methods=['GET'])
def get_users():
try:
conn = get_db_connection()
if not conn:
return jsonify({"success": False, "message": "Database connection failed"}), 500
cursor = conn.cursor()
cursor.execute('''
SELECT UserId, Username, Email, FullName, Role, Status, CreatedDate
FROM Users
ORDER BY CreatedDate DESC
''')
users = []
for row in cursor.fetchall():
users.append({
"UserId": row[0],
"Username": row[1],
"Email": row[2],
"FullName": row[3],
"Role": row[4],
"Status": row[5],
"CreatedDate": str(row[6])
})
return jsonify({"success": True, "data": users}), 200
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
finally:
if conn:
conn.close()
@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
try:
conn = get_db_connection()
if not conn:
return jsonify({"success": False, "message": "Database connection failed"}), 500
cursor = conn.cursor()
cursor.execute('SELECT * FROM Users WHERE UserId = ?', (user_id,))
user = cursor.fetchone()
if user:
return jsonify({
"success": True,
"data": {"UserId": user[0], "Username": user[1]}
}), 200
else:
return jsonify({"success": False, "message": "User not found"}), 404
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
finally:
if conn:
conn.close()
@app.route('/api/users', methods=['POST'])
def create_user():
try:
data = request.get_json()
conn = get_db_connection()
if not conn:
return jsonify({"success": False, "message": "Database connection failed"}), 500
cursor = conn.cursor()
cursor.execute('''
INSERT INTO Users (Username, Password, Email, FullName, Role, IsActive)
VALUES (?, ?, ?, ?, ?, 1)
''', (data['username'], data['password'], data['email'],
data['fullname'], data['role']))
conn.commit()
return jsonify({
"success": True,
"message": "User created"
}), 201
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
finally:
if conn:
conn.close()
# ==========================================
# Routes: Applications
# ==========================================
@app.route('/api/applications', methods=['GET'])
def get_applications():
try:
conn = get_db_connection()
if not conn:
return jsonify({"success": False, "message": "Database connection failed"}), 500
cursor = conn.cursor()
cursor.execute('SELECT * FROM Applications ORDER BY Name')
apps = []
for row in cursor.fetchall():
apps.append({
"AppId": row[0],
"Name": row[1],
"Type": row[2],
"Status": row[3],
"Icon": row[4],
"Description": row[5]
})
return jsonify({"success": True, "data": apps}), 200
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
finally:
if conn:
conn.close()
# ==========================================
# Routes: Accounts
# ==========================================
@app.route('/api/accounts/user/<int:user_id>', methods=['GET'])
def get_accounts(user_id):
try:
conn = get_db_connection()
if not conn:
return jsonify({"success": False, "message": "Database connection failed"}), 500
cursor = conn.cursor()
cursor.execute('''
SELECT a.*, app.Name, app.Type
FROM Accounts a
JOIN Applications app ON a.AppId = app.AppId
WHERE a.UserId = ?
ORDER BY a.CreatedDate DESC
''', (user_id,))
accounts = []
for row in cursor.fetchall():
accounts.append({
"AccountId": row[0],
"UserId": row[1],
"AppId": row[2],
"AccountUsername": row[3],
"AccountPassword": row[4],
"AppName": row[12],
"AppType": row[13]
})
return jsonify({"success": True, "data": accounts}), 200
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
finally:
if conn:
conn.close()
# ==========================================
# Routes: Database Info
# ==========================================
@app.route('/api/database/info', methods=['GET'])
def database_info():
try:
conn = get_db_connection()
if not conn:
return jsonify({"success": False, "message": "Database connection failed"}), 500
cursor = conn.cursor()
# Get tables
cursor.execute('''
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'dbo'
ORDER BY TABLE_NAME
''')
tables = [row[0] for row in cursor.fetchall()]
# Get statistics
cursor.execute('SELECT COUNT(*) FROM Users')
user_count = cursor.fetchone()[0]
cursor.execute('SELECT COUNT(*) FROM Applications')
app_count = cursor.fetchone()[0]
cursor.execute('SELECT COUNT(*) FROM Accounts')
account_count = cursor.fetchone()[0]
return jsonify({
"success": True,
"database": "AccManager",
"server": DB_SERVER,
"tables": tables,
"statistics": {
"users": user_count,
"applications": app_count,
"accounts": account_count
}
}), 200
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
finally:
if conn:
conn.close()
# ==========================================
# Error Handlers
# ==========================================
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Not found"}), 404
@app.errorhandler(500)
def server_error(error):
return jsonify({"error": "Internal server error"}), 500
# ==========================================
# Startup
# ==========================================
if __name__ == '__main__':
print("\n========================================")
print("AccManager Backend Server (Python)")
print("========================================")
print(f"Server: {DB_SERVER}")
print(f"Database: {DB_NAME}")
print(f"User: {DB_USER}")
# Initialize database
print("\nInitializing database...")
if create_tables():
print("✓ Database tables created/verified")
else:
print("✗ Database initialization failed")
print("\n✓ Server running on http://localhost:5000")
print("\nAPI Endpoints:")
print(" /api/health")
print(" /api/auth/login")
print(" /api/users")
print(" /api/applications")
print(" /api/accounts/user/<id>")
print(" /api/database/info")
print("========================================\n")
app.run(debug=True, host='localhost', port=5000)

Binary file not shown.

View File

@@ -1,53 +0,0 @@
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
========================================

79
tailwind.config.js Normal file
View File

@@ -0,0 +1,79 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./public/**/*.{html,js}"
],
darkMode: "class",
theme: {
extend: {
colors: {
"on-secondary-fixed-variant": "#4e5c71",
"on-secondary": "#f8f8ff",
"secondary-fixed-dim": "#c7d5ed",
"surface-variant": "#d9e4ea",
"surface-tint": "#3755c3",
"primary-container": "#dde1ff",
"primary-dim": "#2848b7",
"on-background": "#2a3439",
"surface-container-lowest": "#ffffff",
"tertiary-fixed-dim": "#d4cdee",
"on-tertiary-container": "#514d68",
"error-container": "#fe8983",
"on-secondary-container": "#455367",
"outline": "#717c82",
"on-primary": "#f8f7ff",
"on-primary-container": "#2747b6",
"inverse-primary": "#6d89fa",
"on-surface": "#2a3439",
"primary-fixed": "#dde1ff",
"on-primary-fixed": "#0732a3",
"secondary-dim": "#465468",
"surface-container-high": "#e1e9ee",
"surface-container-highest": "#d9e4ea",
"on-primary-fixed-variant": "#3352c0",
"on-error-container": "#752121",
"secondary": "#526074",
"tertiary-fixed": "#e3dbfd",
"primary": "#3755c3",
"surface-dim": "#cfdce3",
"tertiary": "#605c78",
"on-error": "#fff7f6",
"secondary-fixed": "#d5e3fc",
"error-dim": "#4e0309",
"surface-bright": "#f7f9fb",
"on-surface-variant": "#566166",
"on-tertiary": "#fcf7ff",
"tertiary-container": "#e3dbfd",
"inverse-on-surface": "#9a9d9f",
"on-tertiary-fixed-variant": "#5b5672",
"tertiary-dim": "#54506b",
"outline-variant": "#a9b4b9",
"on-secondary-fixed": "#324053",
"inverse-surface": "#0b0f10",
"on-tertiary-fixed": "#3e3a54",
"primary-fixed-dim": "#cad2ff",
"surface-container": "#e8eff3",
"secondary-container": "#d5e3fc",
"surface-container-low": "#f0f4f7",
"background": "#f7f9fb",
"error": "#9f403d",
"surface": "#f7f9fb"
},
fontFamily: {
headline: ["Manrope", "sans-serif"],
body: ["Inter", "sans-serif"],
label: ["Inter", "sans-serif"]
},
borderRadius: {
DEFAULT: "0.125rem",
lg: "0.25rem",
xl: "0.5rem",
full: "0.75rem"
}
}
},
plugins: [
require("@tailwindcss/forms"),
require("@tailwindcss/container-queries")
]
};

BIN
tmp-import-source.xls Normal file

Binary file not shown.

BIN
tmp-user-import.xls Normal file

Binary file not shown.