diff --git a/sk1/.env.example b/sk1/.env.example new file mode 100644 index 0000000..f8c3a57 --- /dev/null +++ b/sk1/.env.example @@ -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 diff --git a/sk1/Dockerfile b/sk1/Dockerfile new file mode 100644 index 0000000..12b504a --- /dev/null +++ b/sk1/Dockerfile @@ -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"] diff --git a/sk1/README.md b/sk1/README.md new file mode 100644 index 0000000..3bd4fa2 --- /dev/null +++ b/sk1/README.md @@ -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. \ No newline at end of file diff --git a/sk1/app.conf.template b/sk1/app.conf.template new file mode 100644 index 0000000..547674f --- /dev/null +++ b/sk1/app.conf.template @@ -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; + } +} diff --git a/sk1/app.js b/sk1/app.js new file mode 100644 index 0000000..91fe4dc --- /dev/null +++ b/sk1/app.js @@ -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 = ` + ${index + 1} + ${escapeHtml(entry.student_name)} + ${entry.arrival_time.slice(0, 5)} + ${new Date(entry.created_at).toLocaleString()} + + `; + 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(); diff --git a/sk1/docker-compose.yml b/sk1/docker-compose.yml new file mode 100644 index 0000000..45ee3da --- /dev/null +++ b/sk1/docker-compose.yml @@ -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 diff --git a/sk1/index.html b/sk1/index.html new file mode 100644 index 0000000..1faec97 --- /dev/null +++ b/sk1/index.html @@ -0,0 +1,64 @@ + + + + + + Easyattend + + + +
+
+
+ +

EasyAttend - Record and Manage Attendance hassle-free

+

An attendance app for teachers to record student arrivals and remove incorrect entries.

+
+
Live classroom list
+
+ +
+

Add arrival

+
+ + + +
+

+
+ +
+
+
+

Today’s attendance

+

Saved in database.

+
+ +
+
+ + + + + + + + + + + +
#NameArrivalCreatedAction
+
+ +
+
+ + + + diff --git a/sk1/init.sql b/sk1/init.sql new file mode 100644 index 0000000..a0b0233 --- /dev/null +++ b/sk1/init.sql @@ -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 +); diff --git a/sk1/package.json b/sk1/package.json new file mode 100644 index 0000000..d715235 --- /dev/null +++ b/sk1/package.json @@ -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" + } +} diff --git a/sk1/prepare-app.sh b/sk1/prepare-app.sh new file mode 100644 index 0000000..f653226 --- /dev/null +++ b/sk1/prepare-app.sh @@ -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}" diff --git a/sk1/remove-app.sh b/sk1/remove-app.sh new file mode 100644 index 0000000..ac3d512 --- /dev/null +++ b/sk1/remove-app.sh @@ -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" diff --git a/sk1/server.js b/sk1/server.js new file mode 100644 index 0000000..f52bfcf --- /dev/null +++ b/sk1/server.js @@ -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); + }); diff --git a/sk1/styles.css b/sk1/styles.css new file mode 100644 index 0000000..7a83a46 --- /dev/null +++ b/sk1/styles.css @@ -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; } +}