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