files added

This commit is contained in:
Somangsu Mukherjee 2026-05-12 09:09:18 +00:00
parent e61ea0b168
commit 7d5bd7c0cf
13 changed files with 988 additions and 0 deletions

9
sk1/.env.example Normal file
View 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
View 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
View 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 35 per month
Estimated annual price:
USD 3660 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 3660/year
Domain:
USD 0/year
SSL certificate:
USD 0/year
Estimated total:
USD 128.88152.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
View 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
View 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 => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;"
}[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
View 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
View 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>Todays 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
View 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
View 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
View 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
View 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
View 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
View 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; }
}