const express = require('express'); const bodyParser = require('body-parser'); const session = require('express-session'); const QRCode=require('qrcode'); const crypto=require('crypto'); const path=require('path'); const app = express(); app.set('view engine','ejs'); app.use(express.json()); const PORT = 3000; const PWSalt="!SaltyMagic7283715374"; const QRSalt="!SaltyMagic5392370662"; // // ToDo, Actions: // + Login with Password // + Login with Token // + Email Link // + Signup // Change Password // Issue Tickets // Transfer Tickets Complicated page. (One ticket: static with transfer and QR code. Multi: buttons to transfer or show QR code) // Claim Tickets // + Use Ticket // Edit Tickets // // ToDo, Later // Use a templating engine // Store password hashed and salted // Make all HTML look nice // Logging and Replay system // function hashPW(pw) { const hash0=crypto.createHash('sha256'); const hash1=hash0.update(pw+PWSalt); const hash=hash1.digest("base64"); return(hash); } function hashQR(t,ownername) { const hash0=crypto.createHash('sha256'); const hash1=hash0.update(t+QRSalt+ownername); const hash=hash1.digest("base64").slice(0,6); return(hash); } // // In-memory data structures // // There are two ways to log in: with a username/password or with a token. // const users = { "teppy@egenesis.com" : { password: hashPW("x6321872X.1"), superuser:true }, "fallsonfire@gmail.com" : { password: hashPW("indiantreeonfire"), superuser:false } }; const tickets = { "habitat-1" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00, used: false }, "habitat-2" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00 }, "habitat-3" : { owner: "ginachen94@gmail.com", offered: "teppy@egenesis.com", paid: 0.00 }, "habitat-4" : { owner: "teppy@egenesis.com" , offered: "noahstern00@gmail.com", paid: 0.00 }, "habitat-5" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00, used: true }, "habitat-6" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00 }, }; const camps = { "habitat": 0 }; const tokens = { "abc" : { username: "teppy@egenesis.com", expires: 0 } }; const tokensu = { "teppy@egenesis.com" : "abc" }; // Middleware setup app.use(bodyParser.urlencoded({ extended: true })); app.use(session({ secret: 'supersecretkey', resave: false, saveUninitialized: false, })); // Middleware to protect routes function requireLogin(req, res, next) { if (!req.session.username) return res.redirect('/login'); next(); } function requireSuperUser(req,res,next) { console.log(req.session); if (!req.session.superuser) return res.status(403).send(JSON.stringify( { error: "Forbidden without superuser flag" } )); else next(); } app.get('/styles.css', (req, res) => { res.setHeader('Content-Type', 'text/css'); res.sendFile(path.join(__dirname, 'public', 'styles.css')); }); function generateSecureToken(length = 8) { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let token = ''; const array = new Uint8Array(length); crypto.getRandomValues(array); for (let i = 0; i < length; i++) { token += characters.charAt(array[i] % characters.length); } return token; } const postmark = require('postmark'); const client = new postmark.ServerClient('f45bcb9f-6556-4420-9e21-05d16739b5e8'); app.get('/loginpassword',(req,res) => { res.send(`

Sign Up



`); }) app.post('/loginpassword', (req, res) => { const { username, password } = req.body; if (username in users && hashPW(password)==users[username].password) { req.session.username=username; req.session.superuser=users[username].superuser; res.redirect('/transfer'); } else { res.redirect('/transfer'); } }) app.get('/logintoken', (req, res) => { const tok=req.query.token; const tokenData = tokens[tok]; if (!consumeToken(tok)) return res.status(200).send("Missing, Invalid or Expired Token."); req.session.username = tokenData.username; req.session.superuser= users[tokenData.username].superuser; res.redirect('/transfer'); }); app.get('/emaillink',(req,res) => { res.send(`

Email me a login link

` ) }) // These can be owned, offered, of offering function categorizeTickets(username) { let rval="none"; let numtickets=0; let simpleowner=0; let offering=0; let simpleoffered=0; let thet=""; for (const t in tickets) if (!tickets[t].used) { if (tickets[t].owner==username && tickets[t].offered=="") simpleowner+=1; else if (tickets[t].owner==username && tickets[t].offered!=username) offering+=1; else if (tickets[t].owner!=username && tickets[t].offered==username) simpleoffered+=1; if (tickets[t].owner==username || tickets[t].offered==username) numtickets+=1; if (numtickets==1) thet=t; else thet=""; } console.log("Simpleowner: "+simpleowner+" Offering "+offering+" SimpleOffered "+simpleoffered+" Numtickets "+numtickets); if (numtickets==0) return [ "none", ""]; if (numtickets >1) return [ "complex", ""]; if (simpleowner+offering+simpleoffered>1) return [ "error", "" ] if (simpleowner==1) return [ "simpleowner", thet ]; if (offering==1) return [ "simpleoffering", thet ]; if (simpleoffered==1) return [ "simpleoffered", thet ]; return [ "complex", "" ]; } // Big Kahuna // If you have zero tickets, show something saying that // For each ticket owned, display options to offer it, use it, or (eventually) pay for it. // For each ticket offered, show a button to claim it. // Have a button to claim all tickets, if there are multiple offered to you app.get('/transfer', requireLogin, async (req,res) => { let username=req.session.username; const [ cat, ticket ] = categorizeTickets(username); const simpledata={ username: username, ticket: ticket }; console.log("Transfer type "+cat); if (cat=="none") return res.render("notickets",simpledata); if (cat=="error") return res.render("error",simpledata); if (cat=="simpleoffered") return res.render("claimone",simpledata); if (cat=="simpleoffering") return res.render("retractoffer",simpledata); if (cat=="complex") { const edit={ username: username, tickets: {} }; for (const t in tickets) if (tickets[t].owner==username || tickets[t].offered==username) { edit.tickets[t]={}; edit.tickets[t].owner=tickets[t].owner; edit.tickets[t].offered=tickets[t].offered; edit.tickets[t].used=tickets[t].used; } return res.render("transfer",edit); } if (cat=="simpleowner") { try { const hash0=crypto.createHash('sha256'); const hash1=hash0.update(ticket+QRSalt); const hash=hash1.digest("base64").slice(0,6); const dataURL=await QRCode.toDataURL('localhost:3000/useticket?t='+ticket+'&h='+hashQR(ticket,username)); simpledata.qrcode=dataURL; return res.render("simpleowner",simpledata); } catch(err) { res.status(500).send('Error generating QR code'); } } if (cat=="complex") return res.render('transfer',simpledata); }) app.post("/toggle", requireSuperUser, (req,res) => { const ticket=req.body.ticket; const isChecked = req.body.checked; tickets[ticket].used=isChecked; // Perform any server-side logic here res.json({ message: 'Checkbox state received', checked: isChecked }); }) app.post("/updateoffered", requireLogin, (req,res) => { const ticket=req.body.ticket; const offered=req.body.offered; console.log("Ticket: ",ticket); if (tickets[ticket].owner!=req.session.username) res.status(500).send("Ticket "+ticket+" owned by someone else"); else if (tickets[ticket].used) res.status(500).send("Ticket "+ticket+" has already been used"); else { tickets[ticket].offered=offered; res.json({ message: 'Updated owner of '+ticket+' to '+offered }); } }) app.get("/useticket",(req,res) => { let ticket=req.t; let hash=req.h; if (hashQR(ticket,req.session.username)!=hash) res.status(500).send("Ticket "+ticket+" was transferred to "+tickets[ticket].username); else if (tickets[ticket].used) res.status(500).send("Ticket "+ticket+" has already been used."); else { tickets[ticket].used=new Date().toISOString(); res.send("

Welcome, "+tickets[ticket].username+" to Falls on Fire! Ticket "+ticket+" has now been used.

"); } }) function knownEmail(email) { if (users[email]) return true; for (const key in tickets) if (tickets[key].owner==email || tickets[key].offered==email) return true; return false; } app.post('emaillink',(req,res) => { if (!knownUser(req.email)) return res.send("Unknown User"); const tok=createtoken(email); client.sendEmail({ From: 'tickets@fallsonfire.net', To: 'teppy@egenesis.com', Subject: 'Falls on Fire Tickets, Login Link', TextBody: 'Your Login Link: http://www.fallonfire.net/longinlink?token='+tok, HtmlBody: '

Your Login Link:

', }).then(() => { console.log('Email sent'); res.send("

Email sent

"); }).catch((error) => { console.error('Error sending email:', error); res.send("

Email failed

"); }); res.send("Email link sent"); }) app.get('/testemail', (req,res) => { client.sendEmail({ From: 'tickets@fallsonfire.net', To: 'teppy@egenesis.com', Subject: 'Email from Ticketing System', TextBody: 'This is a test email.', HtmlBody: '

This is a test email.

', }).then(() => { console.log('Email sent'); res.send("

Email sent

"); }).catch((error) => { console.error('Error sending email:', error); res.send("

Email failed

"); })}); // Routes app.get('/', (req, res) => { if (req.session.username) { res.send(`

Welcome, ${req.session.username}

Products
FAQ
Log Out `); } else { res.send(`

Welcome to our website!

Log In
Sign Up `); } }); app.get('/signup', (req, res) => { res.send(`

Sign Up



Log In `); }); function consumeToken(tok) { console.log("ConsumeToken 1"); if (!(tok in tokens)) return false; console.log("ConsumeToken 2"); const rval=tokens[tok].expires==0 || tokens[tok].expires>=Date.now(); delete tokensu[tokens[tok].username]; delete tokens[tok]; return rval; } function createToken(username) { const token=generateSecureToken(); tokens[token]={ username: username, expires:0 }; tokensu[username]=token; return token; } app.get('/test', (req, res) => { res.send("Test worked:" + consumeToken("abc")); }) app.post('/signup', (req, res) => { const { username, password } = req.body; if (users[username]) { return res.send('User already exists. Try again'); } users[username] = { password: hashPW(password) }; console.log("Created new account:",username); res.redirect('/login'); }); app.get('/changepassword', (req, res) => { res.send(`

Change Password



Home `); }); app.post('/changepassword', (req, res) => { const { password1, password2 } = req.body; if (!req.session.username) { return res.send('You are not logged inBack'); } if (password1!=password2) { return res.send('Passwords do not matchBack'); } users[req.session.username].password=hashPW(password1); res.redirect('/login'); }); app.get('/login', (req, res) => { res.send(`

Log In



Sign Up `); }); app.post('/login', (req, res) => { const { username, password, superuser } = req.body; if (users[username] && users[username].password === hashPW(password)) { req.session.username = username; req.session.superuser = users[username].superuser; return res.redirect('/transfer'); } res.send('Invalid username or password. Try again'); }); app.get('/logout', (req, res) => { req.session.destroy(() => { res.redirect('/'); }); }); app.post('/qrcode',requireLogin,async (req,res) => { const username=req.session.username; const ticket=req.body.ticket; console.log("Body: ",req.body); console.log("Tickets["+ticket+"]",tickets[ticket]); if (tickets[ticket].owner!=username) return res.status(500).send("Only a ticket owner can generate a QR code"); const URL=await QRCode.toDataURL('localhost:3000/useticket?t='+ticket+'&h='+hashQR(ticket,username)); return res.send({ qrcode: URL }); }) // Protected routes app.get('/products', requireLogin, (req, res) => { res.send(`

Products Page

Here is a list of products!

Home `); }); app.get('/tickets', requireLogin, (req, res) => { res.write(`

Tickets for `+req.session.username+`

`); res.write(`Home`); res.end(); }); app.get('/faq', requireLogin, (req, res) => { res.send(`

FAQ Page

Here are some frequently asked questions.

Home `); }); // Start the server app.listen(PORT, () => { console.log(`Server is running at http://localhost:${PORT}`); });