files added
This commit is contained in:
parent
e61ea0b168
commit
7d5bd7c0cf
9
sk1/.env.example
Normal file
9
sk1/.env.example
Normal file
@ -0,0 +1,9 @@
|
||||
# Copy this file to .env and change the values before running prepare-app.sh
|
||||
DOMAIN_NAME=easyattend.duckdns.org
|
||||
LETSENCRYPT_EMAIL=somangsu.mukherjee@student.tuke.sk
|
||||
POSTGRES_USER=attendance_user
|
||||
POSTGRES_PASSWORD=change_this_password
|
||||
POSTGRES_DB=attendance_db
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
PORT=3000
|
||||
7
sk1/Dockerfile
Normal file
7
sk1/Dockerfile
Normal file
@ -0,0 +1,7 @@
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
COPY server.js ./
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
490
sk1/README.md
Normal file
490
sk1/README.md
Normal file
@ -0,0 +1,490 @@
|
||||
# EasyAttend – Classroom Attendance Checklist Application
|
||||
Author: Somangsu Mukherjee
|
||||
|
||||
## 1. Description of the Application
|
||||
|
||||
EasyAttend is a web-based classroom attendance management application designed for teachers, lecturers, or classroom administrators. The application allows users to record student attendance quickly and efficiently through a clean web interface.
|
||||
|
||||
Main features:
|
||||
|
||||
- Add student names
|
||||
- Record student arrival times
|
||||
- Display attendance records in a structured table
|
||||
- Delete incorrect attendance entries
|
||||
- Store attendance records permanently in a database
|
||||
|
||||
The application is publicly accessible over the Internet and secured with HTTPS encryption.
|
||||
|
||||
---
|
||||
|
||||
## 2. Public Cloud, Cloud Services, Docker Objects, Persistent Volumes and Database Used
|
||||
|
||||
### Public Cloud Platform
|
||||
|
||||
The application is deployed on Microsoft Azure public cloud infrastructure.
|
||||
|
||||
Cloud service used:
|
||||
|
||||
- Microsoft Azure Virtual Machine
|
||||
- Ubuntu Server 24.04 LTS
|
||||
- Azure B2ats v2 virtual machine instance
|
||||
- Public IP address
|
||||
- Azure for Students subscription
|
||||
|
||||
The Azure virtual machine serves as the hosting environment for the complete Docker deployment.
|
||||
|
||||
---
|
||||
|
||||
### Docker Architecture
|
||||
|
||||
The application uses Docker Compose to manage multiple containers.
|
||||
|
||||
Docker objects used:
|
||||
|
||||
#### Frontend Container
|
||||
|
||||
Container name:
|
||||
|
||||
attendance-frontend
|
||||
|
||||
Technology:
|
||||
|
||||
- Nginx
|
||||
- HTML
|
||||
- CSS
|
||||
- JavaScript
|
||||
|
||||
Purpose:
|
||||
|
||||
Serves the user interface and allows interaction with the attendance application.
|
||||
|
||||
---
|
||||
|
||||
#### Backend Container
|
||||
|
||||
Container name:
|
||||
|
||||
attendance-backend
|
||||
|
||||
Technology:
|
||||
|
||||
- Node.js
|
||||
- Express.js
|
||||
|
||||
Purpose:
|
||||
|
||||
Handles application logic, API requests, and communication with the database.
|
||||
|
||||
---
|
||||
|
||||
#### Database Container
|
||||
|
||||
Container name:
|
||||
|
||||
attendance-db
|
||||
|
||||
Technology:
|
||||
|
||||
- PostgreSQL 16
|
||||
|
||||
Purpose:
|
||||
|
||||
Stores attendance records persistently.
|
||||
|
||||
---
|
||||
|
||||
#### Reverse Proxy Container
|
||||
|
||||
Container name:
|
||||
|
||||
attendance-proxy
|
||||
|
||||
Technology:
|
||||
|
||||
- Nginx
|
||||
|
||||
Purpose:
|
||||
|
||||
- Routes traffic to frontend and backend services
|
||||
- Handles HTTPS encryption
|
||||
- Redirects HTTP traffic to HTTPS
|
||||
- Handles SSL certificate integration
|
||||
|
||||
---
|
||||
|
||||
#### Database Administration Container
|
||||
|
||||
Container name:
|
||||
|
||||
attendance-adminer
|
||||
|
||||
Technology:
|
||||
|
||||
- Adminer
|
||||
|
||||
Purpose:
|
||||
|
||||
Provides optional web-based database management.
|
||||
|
||||
---
|
||||
|
||||
### Persistent Volumes
|
||||
|
||||
Persistent Docker volumes used:
|
||||
|
||||
#### PostgreSQL Persistent Volume
|
||||
|
||||
Volume name:
|
||||
|
||||
postgres_data
|
||||
|
||||
Purpose:
|
||||
|
||||
Stores PostgreSQL data files permanently so records remain available after container restart or redeployment.
|
||||
|
||||
---
|
||||
|
||||
#### Nginx Log Volume
|
||||
|
||||
Volume name:
|
||||
|
||||
nginx_logs
|
||||
|
||||
Purpose:
|
||||
|
||||
Stores web access logs for monitoring incoming Internet traffic.
|
||||
|
||||
---
|
||||
|
||||
## 3. Cost Analysis for One Year Operation
|
||||
|
||||
Estimated usage assumptions:
|
||||
|
||||
- 1000 users per day
|
||||
- 50GB total database/storage usage
|
||||
- lightweight application workload
|
||||
|
||||
### Azure Virtual Machine
|
||||
|
||||
Resource:
|
||||
|
||||
Microsoft Azure Virtual Machine
|
||||
|
||||
Configuration:
|
||||
|
||||
- B2ats v2
|
||||
- 2 vCPU
|
||||
- 1 GB RAM
|
||||
|
||||
price:
|
||||
|
||||
USD 7.74 per month
|
||||
|
||||
Annual cost:
|
||||
|
||||
USD 92.88 per year
|
||||
|
||||
Billing interval:
|
||||
|
||||
Monthly
|
||||
|
||||
---
|
||||
|
||||
### Storage
|
||||
|
||||
Resource:
|
||||
|
||||
Azure Managed Disk Storage
|
||||
|
||||
Estimated requirement:
|
||||
|
||||
50GB
|
||||
|
||||
Estimated monthly price:
|
||||
|
||||
USD 3–5 per month
|
||||
|
||||
Estimated annual price:
|
||||
|
||||
USD 36–60 per year
|
||||
|
||||
Billing interval:
|
||||
|
||||
Monthly
|
||||
|
||||
---
|
||||
|
||||
### Domain Name
|
||||
|
||||
Resource:
|
||||
|
||||
DuckDNS
|
||||
|
||||
Price:
|
||||
|
||||
Free
|
||||
|
||||
Billing interval:
|
||||
|
||||
None
|
||||
|
||||
---
|
||||
|
||||
### SSL Certificate
|
||||
|
||||
Resource:
|
||||
|
||||
Let's Encrypt
|
||||
|
||||
Price:
|
||||
|
||||
Free
|
||||
|
||||
Billing interval:
|
||||
|
||||
None
|
||||
|
||||
---
|
||||
|
||||
### Estimated Total Annual Cost
|
||||
|
||||
Virtual machine:
|
||||
|
||||
USD 92.88/year
|
||||
|
||||
Storage:
|
||||
|
||||
USD 36–60/year
|
||||
|
||||
Domain:
|
||||
|
||||
USD 0/year
|
||||
|
||||
SSL certificate:
|
||||
|
||||
USD 0/year
|
||||
|
||||
Estimated total:
|
||||
|
||||
USD 128.88–152.88 per year
|
||||
|
||||
---
|
||||
|
||||
## 4. Description of Uploaded Files
|
||||
|
||||
### docker-compose.yml
|
||||
|
||||
Defines all application containers, networks, persistent volumes, restart policies, and inter-container communication.
|
||||
|
||||
---
|
||||
|
||||
### prepare-app.sh
|
||||
|
||||
Deployment automation script.
|
||||
|
||||
Functions:
|
||||
|
||||
- starts Docker services
|
||||
- builds backend container
|
||||
- initializes containers
|
||||
- prepares networking
|
||||
- prepares application deployment
|
||||
|
||||
---
|
||||
|
||||
### remove-app.sh
|
||||
|
||||
Application removal script.
|
||||
|
||||
Functions:
|
||||
|
||||
- stops Docker containers
|
||||
- removes application services
|
||||
- removes associated Docker networks
|
||||
|
||||
---
|
||||
|
||||
### frontend/index.html
|
||||
|
||||
Main user interface for the application.
|
||||
|
||||
---
|
||||
|
||||
### frontend/styles.css
|
||||
|
||||
Visual styling and layout for the web application.
|
||||
|
||||
---
|
||||
|
||||
### frontend/app.js
|
||||
|
||||
Frontend JavaScript functionality for communicating with backend API and updating attendance records dynamically.
|
||||
|
||||
---
|
||||
|
||||
### backend/Dockerfile
|
||||
|
||||
Defines backend container image configuration.
|
||||
|
||||
---
|
||||
|
||||
### backend/package.json
|
||||
|
||||
Node.js dependency configuration.
|
||||
|
||||
---
|
||||
|
||||
### backend/server.js
|
||||
|
||||
Main backend API implementation.
|
||||
|
||||
Handles:
|
||||
|
||||
- student attendance insertion
|
||||
- attendance retrieval
|
||||
- attendance deletion
|
||||
- database communication
|
||||
|
||||
---
|
||||
|
||||
### db/init.sql
|
||||
|
||||
Initial PostgreSQL database schema definition.
|
||||
|
||||
Creates attendance table structure.
|
||||
|
||||
---
|
||||
|
||||
### nginx/app.conf
|
||||
|
||||
Nginx reverse proxy configuration.
|
||||
|
||||
Handles:
|
||||
|
||||
- HTTP traffic
|
||||
- HTTPS traffic
|
||||
- SSL certificate integration
|
||||
- reverse proxy routing
|
||||
- frontend routing
|
||||
- backend API routing
|
||||
- Adminer routing
|
||||
|
||||
---
|
||||
|
||||
### .gitignore
|
||||
|
||||
Prevents sensitive or unnecessary files from being uploaded to Git.
|
||||
|
||||
Excluded examples:
|
||||
|
||||
- .env
|
||||
- SSL certificates
|
||||
- SQL backup files
|
||||
- SSH private keys
|
||||
|
||||
---
|
||||
|
||||
## 5. Brief Description of Configuration
|
||||
|
||||
Application configuration is managed through Docker Compose and environment variables.
|
||||
|
||||
Configured components include:
|
||||
|
||||
- PostgreSQL credentials
|
||||
- database name
|
||||
- domain name
|
||||
- SSL certificate settings
|
||||
- Docker networking
|
||||
- persistent volume mappings
|
||||
- reverse proxy rules
|
||||
- restart policies
|
||||
|
||||
Sensitive values are stored in:
|
||||
|
||||
.env
|
||||
|
||||
This file is intentionally excluded from Git for security reasons.
|
||||
|
||||
---
|
||||
|
||||
## 6. Instructions to View and Use the Application
|
||||
|
||||
Application URL:
|
||||
|
||||
https://easyattend.duckdns.org
|
||||
|
||||
Instructions:
|
||||
|
||||
1. Open a web browser
|
||||
2. Navigate to the application URL
|
||||
3. Enter a student name
|
||||
4. Select arrival time
|
||||
5. Click "Add student"
|
||||
6. Attendance record will appear in the attendance table
|
||||
7. Use the delete button to remove incorrect records
|
||||
|
||||
---
|
||||
|
||||
## 7. Instructions to Perform Data Backup
|
||||
|
||||
Database backup command:
|
||||
|
||||
```bash
|
||||
docker exec attendance-db pg_dump -U attendance_user attendance_db > attendance_backup.sql
|
||||
```
|
||||
This creates:
|
||||
|
||||
attendance_backup.sql
|
||||
|
||||
This file contains a full backup of the PostgreSQL database.
|
||||
|
||||
---
|
||||
## 8. Instructions to View Access Records from the Internet
|
||||
|
||||
Access logs are available through the Nginx reverse proxy container.
|
||||
|
||||
Command:
|
||||
```bash
|
||||
docker logs attendance-proxy
|
||||
```
|
||||
Displayed information includes:
|
||||
|
||||
client IP addresses
|
||||
timestamps
|
||||
requested URLs
|
||||
HTTP response codes
|
||||
browser user agents
|
||||
|
||||
---
|
||||
## 9. Conditions Required to Run prepare-app.sh and remove-app.sh
|
||||
|
||||
Required conditions:
|
||||
|
||||
Ubuntu Linux environment
|
||||
Docker installed
|
||||
Docker Compose installed
|
||||
Internet connection
|
||||
properly configured .env file
|
||||
valid domain name configuration
|
||||
Docker permissions for executing user
|
||||
complete project file structure present
|
||||
|
||||
Deployment command:
|
||||
```bash
|
||||
./prepare-app.sh
|
||||
```
|
||||
Removal command:
|
||||
```bash
|
||||
./remove-app.sh
|
||||
```
|
||||
---
|
||||
|
||||
## 10. External Resources and Generative Model Usage
|
||||
External Resources :
|
||||
Microsoft Azure documentation, Let's Encrypt documentation, DuckDNS documentation:
|
||||
|
||||
Generative Model Usage: Claude, OpenAI ChatGPT
|
||||
Method of usage:
|
||||
Debugging, architecture planning, troubleshooting, researching
|
||||
|
||||
All implementation, deployment, testing, debugging, and validation actions were manually performed by me.
|
||||
38
sk1/app.conf.template
Normal file
38
sk1/app.conf.template
Normal file
@ -0,0 +1,38 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name easyattend.duckdns.org;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name easyattend.duckdns.org;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/easyattend.duckdns.org/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/easyattend.duckdns.org/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://frontend:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:3000/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /adminer/ {
|
||||
proxy_pass http://adminer:8080/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
78
sk1/app.js
Normal file
78
sk1/app.js
Normal file
@ -0,0 +1,78 @@
|
||||
const API_URL = "/api/attendance";
|
||||
const form = document.getElementById("attendance-form");
|
||||
const nameInput = document.getElementById("student-name");
|
||||
const timeInput = document.getElementById("arrival-time");
|
||||
const body = document.getElementById("attendance-body");
|
||||
const emptyState = document.getElementById("empty-state");
|
||||
const message = document.getElementById("message");
|
||||
const refreshBtn = document.getElementById("refresh-btn");
|
||||
|
||||
function setDefaultTime() {
|
||||
const now = new Date();
|
||||
timeInput.value = now.toTimeString().slice(0, 5);
|
||||
}
|
||||
|
||||
function showMessage(text) {
|
||||
message.textContent = text;
|
||||
setTimeout(() => { message.textContent = ""; }, 2500);
|
||||
}
|
||||
|
||||
async function loadAttendance() {
|
||||
const response = await fetch(API_URL);
|
||||
const entries = await response.json();
|
||||
body.innerHTML = "";
|
||||
emptyState.classList.toggle("hidden", entries.length !== 0);
|
||||
|
||||
entries.forEach((entry, index) => {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>${index + 1}</td>
|
||||
<td><strong>${escapeHtml(entry.student_name)}</strong></td>
|
||||
<td>${entry.arrival_time.slice(0, 5)}</td>
|
||||
<td>${new Date(entry.created_at).toLocaleString()}</td>
|
||||
<td><button class="danger" data-id="${entry.id}">Delete</button></td>
|
||||
`;
|
||||
body.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value).replace(/[&<>'"]/g, char => ({
|
||||
"&": "&", "<": "<", ">": ">", "'": "'", '"': """
|
||||
}[char]));
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const student_name = nameInput.value.trim();
|
||||
const arrival_time = timeInput.value;
|
||||
if (!student_name || !arrival_time) return;
|
||||
|
||||
const response = await fetch(API_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ student_name, arrival_time })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
showMessage("Could not add student.");
|
||||
return;
|
||||
}
|
||||
|
||||
form.reset();
|
||||
setDefaultTime();
|
||||
showMessage("Student added successfully.");
|
||||
loadAttendance();
|
||||
});
|
||||
|
||||
body.addEventListener("click", async (event) => {
|
||||
if (!event.target.matches("button[data-id]")) return;
|
||||
const id = event.target.dataset.id;
|
||||
await fetch(`${API_URL}/${id}`, { method: "DELETE" });
|
||||
showMessage("Entry deleted.");
|
||||
loadAttendance();
|
||||
});
|
||||
|
||||
refreshBtn.addEventListener("click", loadAttendance);
|
||||
setDefaultTime();
|
||||
loadAttendance();
|
||||
75
sk1/docker-compose.yml
Normal file
75
sk1/docker-compose.yml
Normal file
@ -0,0 +1,75 @@
|
||||
services:
|
||||
frontend:
|
||||
image: nginx:1.27-alpine
|
||||
container_name: attendance-frontend
|
||||
restart: always
|
||||
volumes:
|
||||
- ./frontend:/usr/share/nginx/html:ro
|
||||
networks:
|
||||
- attendance-net
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
container_name: attendance-backend
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- attendance-net
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: attendance-db
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||
networks:
|
||||
- attendance-net
|
||||
|
||||
adminer:
|
||||
image: adminer:4
|
||||
container_name: attendance-adminer
|
||||
restart: always
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- attendance-net
|
||||
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
container_name: attendance-proxy
|
||||
restart: always
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/app.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./certbot/www:/var/www/certbot
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- nginx_logs:/var/log/nginx
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
- adminer
|
||||
networks:
|
||||
- attendance-net
|
||||
|
||||
certbot:
|
||||
image: certbot/certbot:latest
|
||||
container_name: attendance-certbot
|
||||
volumes:
|
||||
- ./certbot/www:/var/www/certbot
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
nginx_logs:
|
||||
|
||||
networks:
|
||||
attendance-net:
|
||||
driver: bridge
|
||||
64
sk1/index.html
Normal file
64
sk1/index.html
Normal file
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Easyattend</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<section class="hero">
|
||||
<div>
|
||||
|
||||
<h1>EasyAttend - Record and Manage Attendance hassle-free</h1>
|
||||
<p class="subtitle">An attendance app for teachers to record student arrivals and remove incorrect entries.</p>
|
||||
</div>
|
||||
<div class="badge">Live classroom list</div>
|
||||
</section>
|
||||
|
||||
<section class="card form-card">
|
||||
<h2>Add arrival</h2>
|
||||
<form id="attendance-form">
|
||||
<label>
|
||||
Student name
|
||||
<input id="student-name" type="text" placeholder="Student's name here" required maxlength="80" />
|
||||
</label>
|
||||
<label>
|
||||
Arrival time
|
||||
<input id="arrival-time" type="time" required />
|
||||
</label>
|
||||
<button type="submit">Add student</button>
|
||||
</form>
|
||||
<p id="message" class="message"></p>
|
||||
</section>
|
||||
|
||||
<section class="card list-card">
|
||||
<div class="list-header">
|
||||
<div>
|
||||
<h2>Today’s attendance</h2>
|
||||
<p>Saved in database.</p>
|
||||
</div>
|
||||
<button id="refresh-btn" class="secondary">Refresh</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Arrival</th>
|
||||
<th>Created</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="attendance-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="empty-state" class="empty hidden">No students added yet.</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
6
sk1/init.sql
Normal file
6
sk1/init.sql
Normal file
@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS attendance (
|
||||
id SERIAL PRIMARY KEY,
|
||||
student_name VARCHAR(80) NOT NULL,
|
||||
arrival_time TIME NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
15
sk1/package.json
Normal file
15
sk1/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "attendance-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend API for classroom attendance checklist",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"pg": "^8.12.0"
|
||||
}
|
||||
}
|
||||
47
sk1/prepare-app.sh
Normal file
47
sk1/prepare-app.sh
Normal file
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Missing .env file. Copy .env.example to .env and edit DOMAIN_NAME, LETSENCRYPT_EMAIL and database password."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source .env
|
||||
|
||||
if [ -z "${DOMAIN_NAME:-}" ] || [ -z "${LETSENCRYPT_EMAIL:-}" ]; then
|
||||
echo "DOMAIN_NAME and LETSENCRYPT_EMAIL must be set in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Docker is not installed. Install Docker Engine and Docker Compose plugin first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo "Docker Compose plugin is not available."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p certbot/www certbot/conf backups
|
||||
|
||||
# Generate temporary HTTP-only Nginx config so Certbot can verify the domain.
|
||||
sed "s/\${DOMAIN_NAME}/${DOMAIN_NAME}/g" nginx/http-only.conf.template > nginx/app.conf
|
||||
|
||||
# First start Nginx on HTTP so Certbot can verify the domain.
|
||||
docker compose up -d --build frontend backend db adminer nginx
|
||||
|
||||
echo "Requesting HTTPS certificate for ${DOMAIN_NAME}..."
|
||||
docker compose run --rm certbot certonly \
|
||||
--webroot \
|
||||
--webroot-path=/var/www/certbot \
|
||||
--email "${LETSENCRYPT_EMAIL}" \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
-d "${DOMAIN_NAME}"
|
||||
|
||||
# Replace temporary config with final HTTPS config and restart Nginx.
|
||||
sed "s/\${DOMAIN_NAME}/${DOMAIN_NAME}/g" nginx/app.conf.template > nginx/app.conf
|
||||
docker compose restart nginx
|
||||
|
||||
echo "Application prepared successfully. Open: https://${DOMAIN_NAME}"
|
||||
7
sk1/remove-app.sh
Normal file
7
sk1/remove-app.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
docker compose down
|
||||
|
||||
echo "Application containers removed."
|
||||
echo "Database volume is kept for safety. To delete all data too, run: docker compose down -v"
|
||||
77
sk1/server.js
Normal file
77
sk1/server.js
Normal file
@ -0,0 +1,77 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const { Pool } = require('pg');
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'db',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: process.env.POSTGRES_DB,
|
||||
});
|
||||
|
||||
async function initDatabase() {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS attendance (
|
||||
id SERIAL PRIMARY KEY,
|
||||
student_name VARCHAR(80) NOT NULL,
|
||||
arrival_time TIME NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', service: 'attendance-backend' });
|
||||
});
|
||||
|
||||
app.get('/api/attendance', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM attendance ORDER BY created_at DESC, id DESC');
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Failed to load attendance entries' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/attendance', async (req, res) => {
|
||||
try {
|
||||
const { student_name, arrival_time } = req.body;
|
||||
if (!student_name || !arrival_time) {
|
||||
return res.status(400).json({ error: 'student_name and arrival_time are required' });
|
||||
}
|
||||
const result = await pool.query(
|
||||
'INSERT INTO attendance (student_name, arrival_time) VALUES ($1, $2) RETURNING *',
|
||||
[student_name, arrival_time]
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Failed to create attendance entry' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/attendance/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.query('DELETE FROM attendance WHERE id = $1', [req.params.id]);
|
||||
res.json({ deleted: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Failed to delete attendance entry' });
|
||||
}
|
||||
});
|
||||
|
||||
initDatabase()
|
||||
.then(() => app.listen(port, () => console.log(`Attendance API running on port ${port}`)))
|
||||
.catch((error) => {
|
||||
console.error('Database initialization failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
75
sk1/styles.css
Normal file
75
sk1/styles.css
Normal file
@ -0,0 +1,75 @@
|
||||
:root {
|
||||
--bg: #f4f7fb;
|
||||
--card: #ffffff;
|
||||
--text: #172033;
|
||||
--muted: #6b7280;
|
||||
--primary: #2563eb;
|
||||
--primary-dark: #1d4ed8;
|
||||
--danger: #dc2626;
|
||||
--border: #e5e7eb;
|
||||
--shadow: 0 20px 45px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: radial-gradient(circle at top left, #dbeafe, transparent 35%), var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.page {
|
||||
width: min(1100px, 92vw);
|
||||
margin: 0 auto;
|
||||
padding: 48px 0;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
h1 { margin: 0; font-size: clamp(34px, 5vw, 58px); line-height: 1; }
|
||||
h2 { margin: 0 0 16px; font-size: 24px; }
|
||||
.subtitle { max-width: 650px; color: var(--muted); font-size: 18px; line-height: 1.6; }
|
||||
.badge { background: #ecfdf5; color: #047857; padding: 12px 18px; border-radius: 999px; font-weight: 700; white-space: nowrap; }
|
||||
.card { background: rgba(255,255,255,0.92); border: 1px solid var(--border); border-radius: 28px; padding: 26px; box-shadow: var(--shadow); margin-bottom: 24px; }
|
||||
|
||||
form { display: grid; grid-template-columns: 1.5fr 0.8fr auto; gap: 16px; align-items: end; }
|
||||
label { display: grid; gap: 8px; color: var(--muted); font-weight: 600; }
|
||||
input { width: 100%; border: 1px solid var(--border); border-radius: 16px; padding: 14px 16px; font-size: 16px; outline: none; background: #fff; }
|
||||
input:focus { border-color: var(--primary); box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12); }
|
||||
button { border: 0; border-radius: 16px; padding: 14px 20px; background: var(--primary); color: white; font-weight: 800; cursor: pointer; font-size: 15px; transition: 0.2s ease; }
|
||||
button:hover { background: var(--primary-dark); transform: translateY(-1px); }
|
||||
button.secondary { background: #eef2ff; color: var(--primary-dark); }
|
||||
button.danger { background: #fee2e2; color: var(--danger); padding: 10px 14px; }
|
||||
button.danger:hover { background: #fecaca; }
|
||||
.message { min-height: 22px; color: var(--primary-dark); font-weight: 700; }
|
||||
.list-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 18px; }
|
||||
.list-header p { margin: 4px 0 0; color: var(--muted); }
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 16px 12px; border-bottom: 1px solid var(--border); }
|
||||
th { color: var(--muted); font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
td { font-size: 16px; }
|
||||
.empty { text-align: center; color: var(--muted); padding: 30px; }
|
||||
.hidden { display: none; }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.hero, .list-header { align-items: flex-start; flex-direction: column; }
|
||||
form { grid-template-columns: 1fr; }
|
||||
.badge { white-space: normal; }
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user