Compare commits
31 Commits
0c1ca1d923
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 395b1f6e85 | |||
| 9f14491562 | |||
| d88aa39bd6 | |||
| 8b2a9d7afe | |||
| 197186eac8 | |||
| 8bd67200ce | |||
| 4fb7f412bf | |||
| bc7a484a01 | |||
| d4800beb67 | |||
| 3961514f6c | |||
| 9526628334 | |||
| 6dc2391858 | |||
| bcc22b1971 | |||
| 61c4415ff7 | |||
| 1408294922 | |||
| 5f3e38a3d4 | |||
| a8933011dd | |||
| aada849671 | |||
| 3b813a85da | |||
| 30e326f92a | |||
| ee10a7e480 | |||
| e59785e06b | |||
| 27c2a4c51e | |||
| a78769cfde | |||
| 5a7bf191d0 | |||
| b3c1283694 | |||
| 6c2e89dc93 | |||
| c823549458 | |||
| d09ba3d2ad | |||
| 58dbefa155 | |||
| 900a569c51 |
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
.vscode
|
||||
.idea
|
||||
.vs
|
||||
.env
|
||||
docker-compose.yml
|
||||
27
.env
27
.env
@@ -1,6 +1,15 @@
|
||||
# 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
|
||||
|
||||
# SQL Server Configuration
|
||||
@@ -8,8 +17,18 @@ DB_SERVER=172.20.235.176
|
||||
DB_USER=sa
|
||||
DB_PASSWORD=robotics@2022
|
||||
DB_NAME=AccManager
|
||||
DB_ENCRYPT=true
|
||||
DB_ENCRYPT=false
|
||||
DB_TRUST_CERTIFICATE=true
|
||||
DB_CONNECT_TIMEOUT=30000
|
||||
|
||||
# Application
|
||||
NODE_ENV=development
|
||||
# Security
|
||||
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
|
||||
|
||||
@@ -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
296
DEPLOYMENT_GUIDE.md
Normal 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
15
Dockerfile
Normal 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
186
README.md
@@ -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ì
|
||||
|
||||
```
|
||||
Server IP: 172.20.235.176
|
||||
Database: AccManager
|
||||
User: sa
|
||||
Password: robotics@2020
|
||||
Port: 1433 (default)
|
||||
```
|
||||
- Quản lý danh sách người dùng trong nội bộ.
|
||||
- Quản lý danh sách ứng dụng đang sử dụng trong công ty.
|
||||
- Gán tài khoản truy cập ứng dụng cho từng người dùng.
|
||||
- Theo dõi thông tin tài khoản rõ ràng, tránh thất lạc.
|
||||
|
||||
### 👤 Default Admin Account
|
||||
## Ai sẽ sử dụng
|
||||
|
||||
```
|
||||
Username: admin
|
||||
Password: admin
|
||||
Role: admin
|
||||
Status: Active
|
||||
```
|
||||
- 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.
|
||||
- Người dùng thông thường: xem các tài khoản được cấp cho mình.
|
||||
|
||||
## 📋 Database Tables Created
|
||||
## Cách sử dụng nhanh
|
||||
|
||||
### 1. **Users** - User Management
|
||||
- Stores login credentials and user roles
|
||||
- Default admin user: admin/admin
|
||||
1. Đăng nhập vào hệ thống.
|
||||
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ự.
|
||||
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
|
||||
- 4 sample applications pre-loaded:
|
||||
- AWS (Cloud) - online
|
||||
- GitHub (VCS) - online
|
||||
- Google Workspace (Collaboration) - online
|
||||
- Nginx Proxy (Infra) - offline
|
||||
## Quy trình vận hành gợi ý
|
||||
|
||||
### 3. **Accounts** - Credential Storage
|
||||
- Stores credentials for each user-application combination
|
||||
- Linked to Users and Applications tables
|
||||
1. Khi có nhân sự mới:
|
||||
- Tạo người dùng.
|
||||
- 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
|
||||
- Logs all INSERT, UPDATE, DELETE operations
|
||||
- User actions tracked for security
|
||||
2. Khi thay đổi công việc:
|
||||
- Cập nhật lại danh sách ứng dụng được cấp.
|
||||
- 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:**
|
||||
- `server.js` - Main server file
|
||||
- `package.json` - Dependencies
|
||||
- Giảm thất lạc thông tin tài khoản.
|
||||
- Rõ ràng trách nhiệm ai đang dùng tài khoản nào.
|
||||
- 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:**
|
||||
```bash
|
||||
# 1. Install Node.js from https://nodejs.org/
|
||||
# 2. Install dependencies
|
||||
npm install
|
||||
## Tài liệu liên quan
|
||||
|
||||
# 3. Run server
|
||||
npm start
|
||||
- Hướng dẫn triển khai: [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)
|
||||
|
||||
# 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
|
||||
python server_python.py
|
||||
|
||||
# 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
|
||||
Phiên bản tài liệu: 4.0.0
|
||||
Cập nhật: Tháng 4 năm 2026
|
||||
|
||||
220
RUN_COMMANDS.md
220
RUN_COMMANDS.md
@@ -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! 🚀**
|
||||
294
SETUP_GUIDE.md
294
SETUP_GUIDE.md
@@ -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
4862
backend/server.js
Normal file
File diff suppressed because it is too large
Load Diff
270
codedialog.html
270
codedialog.html
@@ -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&family=Inter:wght@400;500;600&display=swap"
|
||||
rel="stylesheet" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
||||
rel="stylesheet" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
||||
rel="stylesheet" />
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"on-error-container": "#752121",
|
||||
"background": "#f7f9fb",
|
||||
"secondary-dim": "#465468",
|
||||
"tertiary-dim": "#54506b",
|
||||
"outline": "#717c82",
|
||||
"on-secondary-container": "#455367",
|
||||
"error-dim": "#4e0309",
|
||||
"primary-fixed": "#dde1ff",
|
||||
"on-primary-fixed": "#0732a3",
|
||||
"surface-variant": "#d9e4ea",
|
||||
"primary-dim": "#2848b7",
|
||||
"on-tertiary": "#fcf7ff",
|
||||
"surface": "#f7f9fb",
|
||||
"primary-container": "#dde1ff",
|
||||
"inverse-on-surface": "#9a9d9f",
|
||||
"error": "#9f403d",
|
||||
"on-primary": "#f8f7ff",
|
||||
"on-surface-variant": "#566166",
|
||||
"tertiary-container": "#e3dbfd",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-dim": "#cfdce3",
|
||||
"tertiary-fixed-dim": "#d4cdee",
|
||||
"inverse-surface": "#0b0f10",
|
||||
"on-primary-fixed-variant": "#3352c0",
|
||||
"surface-tint": "#3755c3",
|
||||
"secondary-fixed": "#d5e3fc",
|
||||
"secondary-fixed-dim": "#c7d5ed",
|
||||
"secondary": "#526074",
|
||||
"tertiary-fixed": "#e3dbfd",
|
||||
"on-secondary-fixed-variant": "#4e5c71",
|
||||
"on-primary-container": "#2747b6",
|
||||
"primary-fixed-dim": "#cad2ff",
|
||||
"surface-container-highest": "#d9e4ea",
|
||||
"surface-container-low": "#f0f4f7",
|
||||
"surface-container": "#e8eff3",
|
||||
"on-tertiary-fixed-variant": "#5b5672",
|
||||
"secondary-container": "#d5e3fc",
|
||||
"inverse-primary": "#6d89fa",
|
||||
"outline-variant": "#a9b4b9",
|
||||
"on-secondary-fixed": "#324053",
|
||||
"on-surface": "#2a3439",
|
||||
"surface-container-high": "#e1e9ee",
|
||||
"on-background": "#2a3439",
|
||||
"on-tertiary-fixed": "#3e3a54",
|
||||
"primary": "#3755c3",
|
||||
"on-tertiary-container": "#514d68",
|
||||
"on-error": "#fff7f6",
|
||||
"on-secondary": "#f8f7ff",
|
||||
"tertiary": "#605c78",
|
||||
"error-container": "#fe8983",
|
||||
"surface-bright": "#f7f9fb"
|
||||
},
|
||||
fontFamily: {
|
||||
"headline": ["Manrope"],
|
||||
"body": ["Inter"],
|
||||
"label": ["Inter"]
|
||||
},
|
||||
borderRadius: { "DEFAULT": "0.125rem", "lg": "0.25rem", "xl": "0.5rem", "full": "0.75rem" },
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
}
|
||||
|
||||
.glass-overlay {
|
||||
backdrop-blur: 12px;
|
||||
background: rgba(247, 249, 251, 0.8);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-background text-on-surface overflow-hidden h-screen flex flex-col">
|
||||
<!-- Blurred Background (Mocking the Accounts Management Table) -->
|
||||
<div class="fixed inset-0 z-0 blur-[8px] opacity-40 pointer-events-none select-none overflow-hidden flex flex-col">
|
||||
<!-- Mock TopNavBar (From JSON Guidance) -->
|
||||
<header
|
||||
class="flex justify-between items-center w-full px-8 h-16 bg-[#f7f9fb] text-[#5a6a72] font-['Manrope'] text-sm tracking-wide font-medium">
|
||||
<div class="text-lg font-extrabold tracking-tighter text-[#2a3439]">Sentinel Accounts</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<span class="material-symbols-outlined">notifications</span>
|
||||
<span class="material-symbols-outlined">help_outline</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex flex-1">
|
||||
<!-- Mock SideNavBar (From JSON Guidance) -->
|
||||
<aside
|
||||
class="flex flex-col h-full py-6 w-64 bg-[#f0f4f7] text-[#5a6a72] font-['Inter'] text-[0.875rem] font-medium">
|
||||
<div class="px-6 mb-8">
|
||||
<div class="text-sm font-black uppercase tracking-[0.05em] text-[#2a3439]">Architectural Sentinel
|
||||
</div>
|
||||
<div class="text-xs text-on-surface-variant opacity-70">Enterprise Security</div>
|
||||
</div>
|
||||
<nav class="flex-1">
|
||||
<div
|
||||
class="flex items-center gap-3 px-6 py-3 bg-white text-[#3755c3] font-bold border-l-4 border-[#3755c3]">
|
||||
<span class="material-symbols-outlined" data-icon="vpn_key">vpn_key</span> Account Access
|
||||
</div>
|
||||
<div class="flex items-center gap-3 px-6 py-3">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span> Dashboard
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<!-- Mock Table Stage -->
|
||||
<main class="flex-1 p-8 bg-background">
|
||||
<div class="h-12 w-48 bg-surface-container-high rounded-xl mb-6"></div>
|
||||
<div
|
||||
class="bg-surface-container-lowest rounded-xl shadow-sm border border-outline-variant/10 overflow-hidden">
|
||||
<div
|
||||
class="h-12 border-b border-outline-variant/10 bg-surface-container-low px-6 flex items-center gap-4">
|
||||
<div class="h-4 w-4 bg-outline-variant/20 rounded"></div>
|
||||
<div class="h-4 w-24 bg-outline-variant/20 rounded"></div>
|
||||
<div class="h-4 w-32 bg-outline-variant/20 rounded ml-auto"></div>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="h-10 w-full bg-surface-container-low/50 rounded-lg"></div>
|
||||
<div class="h-10 w-full bg-surface-container-low/50 rounded-lg"></div>
|
||||
<div class="h-10 w-full bg-surface-container-low/50 rounded-lg"></div>
|
||||
<div class="h-10 w-full bg-surface-container-low/50 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal Overlay -->
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-on-surface/30 backdrop-blur-[2px]">
|
||||
<!-- Dialog Container -->
|
||||
<div
|
||||
class="w-full max-w-[640px] bg-surface-container-lowest rounded-xl shadow-[0px_12px_32px_rgba(42,52,57,0.12)] border-none overflow-hidden">
|
||||
<!-- Modal Header -->
|
||||
<div class="px-8 py-6 border-b border-surface-container flex justify-between items-center">
|
||||
<div>
|
||||
<span
|
||||
class="text-[0.625rem] font-bold uppercase tracking-[0.1em] text-on-surface-variant block mb-1">Resource
|
||||
Details</span>
|
||||
<h2 class="text-2xl font-extrabold text-on-surface tracking-tight">Account Details</h2>
|
||||
</div>
|
||||
<button
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-container transition-colors text-on-surface-variant">
|
||||
<span class="material-symbols-outlined" data-icon="close">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Modal Content (Bento-style layout for metadata) -->
|
||||
<div class="p-8 space-y-8">
|
||||
<!-- Service Info Section -->
|
||||
<div class="flex items-center gap-5 p-5 bg-surface-container-low rounded-xl">
|
||||
<div class="w-14 h-14 bg-white rounded-xl shadow-sm flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-primary text-3xl" data-icon="cloud">cloud</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-0.5">Cloud
|
||||
Infrastructure</div>
|
||||
<div class="text-xl font-bold text-on-surface">AWS Production</div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<span
|
||||
class="px-3 py-1 bg-primary-container text-on-primary-container text-[0.7rem] font-bold rounded-full uppercase tracking-tighter">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Core Details Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Username Field -->
|
||||
<div class="space-y-2">
|
||||
<label
|
||||
class="text-[0.65rem] font-bold uppercase tracking-widest text-on-surface-variant ml-1">Username</label>
|
||||
<div
|
||||
class="flex items-center h-12 px-4 bg-surface-container-highest rounded-lg border border-transparent focus-within:border-primary/40 focus-within:shadow-[0_0_0_2px_rgba(55,85,195,0.05)] transition-all">
|
||||
<span class="material-symbols-outlined text-on-surface-variant mr-3 text-sm"
|
||||
data-icon="alternate_email">alternate_email</span>
|
||||
<span class="text-sm font-medium text-on-surface">admin.aws_prod</span>
|
||||
<button class="ml-auto text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined text-base"
|
||||
data-icon="content_copy">content_copy</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Password Field -->
|
||||
<div class="space-y-2">
|
||||
<label
|
||||
class="text-[0.65rem] font-bold uppercase tracking-widest text-on-surface-variant ml-1">Password</label>
|
||||
<div
|
||||
class="flex items-center h-12 px-4 bg-surface-container-highest rounded-lg border border-transparent focus-within:border-primary/40 focus-within:shadow-[0_0_0_2px_rgba(55,85,195,0.05)] transition-all">
|
||||
<span class="material-symbols-outlined text-on-surface-variant mr-3 text-sm"
|
||||
data-icon="lock">lock</span>
|
||||
<span class="text-sm font-medium text-on-surface">•••••••••••••••</span>
|
||||
<button class="ml-auto text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined text-base"
|
||||
data-icon="visibility">visibility</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Metadata Row (Asymmetric Bento) -->
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<!-- Owner Card -->
|
||||
<div class="col-span-7 bg-surface-container-low/50 p-4 rounded-xl flex items-center gap-4">
|
||||
<img alt="Alex Rivera Profile" class="w-10 h-10 rounded-full object-cover ring-2 ring-white"
|
||||
data-alt="Close-up professional portrait of a male systems administrator with a friendly expression in a modern office environment"
|
||||
src="https://lh3.googleusercontent.com/aida-public/AB6AXuBi4gNrkG6OjxYer2iM7vtnmB1_dhArLqll8N46GWZ4YDXLfnwRIIf_bLhZRcMjHCxtKLivBh_JJMTnGRO4kIj0ZCtbVZ61SFhSJvZlPE3ZgNmNCCh7bDXDeFgdWnHKhWAcjDcpLmO02gp5HCU_6GJpLNdIU3pJosKGJsVW_hAhIfp8OYJcepHHf_23k3eQ9ZxkOP4ZR4qu2PU6ZmO2qTCVlJCZVtB-x6RC3YsjcpMNwpyIhSNCIcAvRKTOfU_cb2vtO6t9oD38b6o" />
|
||||
<div>
|
||||
<div class="text-[0.6rem] font-bold uppercase text-on-surface-variant mb-0.5">Account Owner
|
||||
</div>
|
||||
<div class="text-sm font-semibold text-on-surface">Alex Rivera</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Date Card -->
|
||||
<div class="col-span-5 bg-surface-container-low/50 p-4 rounded-xl">
|
||||
<div class="text-[0.6rem] font-bold uppercase text-on-surface-variant mb-0.5">Date Created</div>
|
||||
<div class="text-sm font-semibold text-on-surface flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-base text-on-surface-variant"
|
||||
data-icon="calendar_today">calendar_today</span>
|
||||
Oct 24, 2023
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer Actions -->
|
||||
<div class="px-8 py-6 bg-surface-container-low flex items-center justify-end gap-3">
|
||||
<button
|
||||
class="px-5 h-11 text-sm font-bold text-on-secondary-container hover:bg-surface-container transition-all rounded-lg">
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
class="px-6 h-11 text-sm font-bold text-on-primary bg-gradient-to-br from-primary to-primary-dim rounded-lg shadow-sm hover:opacity-90 active:scale-[0.98] transition-all flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-base" data-icon="edit">edit</span>
|
||||
Edit Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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
|
||||
@@ -81,7 +81,327 @@ BEGIN
|
||||
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')
|
||||
BEGIN
|
||||
@@ -100,7 +420,7 @@ BEGIN
|
||||
END
|
||||
|
||||
-- ===========================================
|
||||
-- 5. CREATE INDEXES
|
||||
-- 10. CREATE INDEXES
|
||||
-- ===========================================
|
||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
|
||||
BEGIN
|
||||
@@ -117,10 +437,65 @@ BEGIN
|
||||
CREATE INDEX IX_Accounts_AppId ON Accounts(AppId);
|
||||
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.';
|
||||
|
||||
-- ===========================================
|
||||
-- 6. INSERT INITIAL DATA
|
||||
-- 11. INSERT INITIAL DATA
|
||||
-- ===========================================
|
||||
|
||||
-- Check if admin user exists
|
||||
@@ -144,7 +519,7 @@ BEGIN
|
||||
END
|
||||
|
||||
-- ===========================================
|
||||
-- 7. DISPLAY DATABASE INFORMATION
|
||||
-- 12. DISPLAY DATABASE INFORMATION
|
||||
-- ===========================================
|
||||
PRINT '';
|
||||
PRINT '========================================';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Sqlcmd: Error: Microsoft ODBC Driver 17 for SQL Server : Login failed for user 'sa'..
|
||||
26
docker-compose.image.yml
Normal file
26
docker-compose.image.yml
Normal 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
28
docker-compose.yml
Normal 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}
|
||||
134
docs/README.md
134
docs/README.md
@@ -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
|
||||
@@ -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
830
js/app.js
@@ -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();
|
||||
});
|
||||
187
modals.html
187
modals.html
@@ -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
1215
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -2,21 +2,37 @@
|
||||
"name": "accmanager-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend server for AccManager application",
|
||||
"main": "server.js",
|
||||
"main": "backend/server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
"start": "node backend/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": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"mssql": "^9.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1"
|
||||
"multer": "^2.1.1",
|
||||
"nodemailer": "^8.0.4",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
176
pages/index.html
176
pages/index.html
@@ -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>
|
||||
254
pages/login.html
254
pages/login.html
@@ -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
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
1
public/css/main.css
Normal file
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
3
public/css/tailwind.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
7288
public/js/app.js
Normal file
7288
public/js/app.js
Normal file
File diff suppressed because it is too large
Load Diff
670
public/modals.html
Normal file
670
public/modals.html
Normal 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>
|
||||
30
public/pages/accounts.html
Normal file
30
public/pages/accounts.html
Normal 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>
|
||||
30
public/pages/applications.html
Normal file
30
public/pages/applications.html
Normal 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
310
public/pages/index.html
Normal 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
710
public/pages/login.html
Normal 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
46
public/pages/users.html
Normal 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>
|
||||
70
public/pages/verify-email.html
Normal file
70
public/pages/verify-email.html
Normal 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>
|
||||
@@ -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
548
server.js
@@ -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();
|
||||
480
server_python.py
480
server_python.py
@@ -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.
@@ -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
79
tailwind.config.js
Normal 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
BIN
tmp-import-source.xls
Normal file
Binary file not shown.
BIN
tmp-user-import.xls
Normal file
BIN
tmp-user-import.xls
Normal file
Binary file not shown.
Reference in New Issue
Block a user