BOU_2026 / server.js
Ansaribinhyder's picture
Upload 5 files
bb5c88a verified
Raw History Blame Contribute Delete
5.22 kB
// BOU 2026 - Best Project voting app
// Serves the voting page and logs every vote (Name, UID, Email, Project, Timestamp)
// into an Excel file on disk, one row per voter (re-voting updates their row).
const path = require("path");
const fs = require("fs");
const express = require("express");
const ExcelJS = require("exceljs");
const PORT = process.env.PORT || 3000;
const ADMIN_KEY = process.env.ADMIN_KEY || "change-me"; // set a real secret before deploying
const VOTES_FILE = path.join(__dirname, "data", "votes.xlsx");
const PROJECTS = [
{ id: "1", name: "Global Account – DAS (Adecco)", who: "Sathiaseelan Arumugam" },
{ id: "2", name: "PCB & IC Package Team", who: "Syam" },
{ id: "3", name: "Product Engineering / Embedded", who: "Munajat Abdul Hadi" },
{ id: "4", name: "CIS", who: "Shunmuganainar Chockalingam" },
{ id: "5", name: "ATE", who: "Kother" },
{ id: "6", name: "Manufacturing POD", who: "Rohan Chowdhary" },
{ id: "7", name: "Manufacturing", who: "Hasrul Hasni" },
];
const PROJECT_IDS = new Set(PROJECTS.map((p) => p.id));
const HEADERS = ["Name", "UID", "Email", "Project ID", "Project Name", "Timestamp"];
// --- tiny write queue so concurrent votes never corrupt the Excel file ---
let writeQueue = Promise.resolve();
function withWriteLock(fn) {
const result = writeQueue.then(fn, fn);
writeQueue = result.catch(() => {});
return result;
}
async function loadWorkbook() {
const wb = new ExcelJS.Workbook();
if (fs.existsSync(VOTES_FILE)) {
await wb.xlsx.readFile(VOTES_FILE);
} else {
fs.mkdirSync(path.dirname(VOTES_FILE), { recursive: true });
const ws = wb.addWorksheet("Votes");
ws.addRow(HEADERS);
ws.getRow(1).font = { bold: true };
}
const ws = wb.getWorksheet("Votes") || wb.addWorksheet("Votes");
if (ws.rowCount === 0) {
ws.addRow(HEADERS);
ws.getRow(1).font = { bold: true };
}
return { wb, ws };
}
async function recordVote({ name, uid, email, projectId }) {
return withWriteLock(async () => {
const { wb, ws } = await loadWorkbook();
const project = PROJECTS.find((p) => p.id === projectId);
// find an existing row for this UID (column 2) so re-voting updates it
let existingRow = null;
ws.eachRow((row, rowNumber) => {
if (rowNumber === 1) return;
if (String(row.getCell(2).value) === String(uid)) existingRow = row;
});
const values = [name, uid, email, project.id, project.name, new Date().toISOString()];
if (existingRow) {
existingRow.values = [, ...values]; // keep 1-based column alignment
} else {
ws.addRow(values);
}
await wb.xlsx.writeFile(VOTES_FILE);
});
}
async function tallyResults() {
const { ws } = await loadWorkbook();
const counts = {};
let total = 0;
const voters = [];
ws.eachRow((row, rowNumber) => {
if (rowNumber === 1) return;
const projectId = String(row.getCell(4).value || "");
if (!projectId) return;
counts[projectId] = (counts[projectId] || 0) + 1;
total += 1;
voters.push({
name: row.getCell(1).value,
uid: row.getCell(2).value,
email: row.getCell(3).value,
projectId,
});
});
return { counts, total, voters };
}
// --- app ---
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, "public")));
app.get("/api/projects", (req, res) => res.json(PROJECTS));
app.post("/api/vote", async (req, res) => {
const { name, uid, email, projectId } = req.body || {};
if (!name || !uid || !email || !projectId) {
return res.status(400).json({ ok: false, error: "Name, UID, email and a project choice are all required." });
}
if (!/^\S+@\S+\.\S+$/.test(email)) {
return res.status(400).json({ ok: false, error: "That email address doesn't look valid." });
}
if (!PROJECT_IDS.has(String(projectId))) {
return res.status(400).json({ ok: false, error: "Unknown project." });
}
try {
await recordVote({ name: String(name).trim(), uid: String(uid).trim(), email: String(email).trim(), projectId: String(projectId) });
res.json({ ok: true });
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "Could not save your vote. Please try again." });
}
});
function requireAdmin(req, res, next) {
const key = req.query.key || req.headers["x-admin-key"];
if (key !== ADMIN_KEY) return res.status(401).json({ ok: false, error: "Invalid admin key." });
next();
}
app.get("/api/results", requireAdmin, async (req, res) => {
try {
const { counts, total } = await tallyResults();
res.json({ ok: true, projects: PROJECTS, counts, total });
} catch (err) {
console.error(err);
res.status(500).json({ ok: false, error: "Could not load results." });
}
});
app.get("/api/export", requireAdmin, async (req, res) => {
if (!fs.existsSync(VOTES_FILE)) {
return res.status(404).json({ ok: false, error: "No votes recorded yet." });
}
res.download(VOTES_FILE, "bou2026-votes.xlsx");
});
app.listen(PORT, () => {
console.log(`BOU 2026 voting app listening on port ${PORT}`);
if (ADMIN_KEY === "change-me") {
console.warn("WARNING: set a real ADMIN_KEY environment variable before sharing this app.");
}
});