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()); app.use(express.static('public')); const PORT = 3000; const PWSalt="!SaltyMagic7283715374"; const QRSalt="!SaltyMagic5392370662"; // // ToDo, Actions: // + Login with Password // + Login with Token // + Email Link // + Display Many Tickets (User) // + Display One Ticket (User) // + Camp Editor // + Use Ticket // + Claim Any Tickets // Purge Revoked Function (Admin) // Signup // Change Password // Issue Tickets from Camp Admin Page // // 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 } }; // Status can be "i"=issued, "u"=used, "r"=revoked" const tickets = { "habitat-1" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"i" }, "habitat-2" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"u" }, "habitat-3" : { owner: "ginachen94@gmail.com", offered: "teppyx@egenesis.com", paid: 0.00, status:"i" }, "habitat-4" : { owner: "teppyx@egenesis.com" , offered: "noahstern00@gmail.com", paid: 0.00, status:"i" }, "habitat-5" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"r" }, "habitat-6" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"i" }, }; const camps = { "habitat": { leader: "teppy@egenesis.com", lastid:6 } }; 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 next(); req.session.returnTo=req.originalUrl; return res.redirect('/login'); } function requireSuperUser(req,res,next) { if (req.session.superuser) return next(); req.session.returnTo=req.originalUrl; console.log("In requireSuperUser: ",req.session); return res.redirect('/login'); } 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('/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].status=="i") { 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=""; } 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", "" ]; } app.get('/camps',requireSuperUser, (req,res) => { const camplist={}; for (const c in camps) { camplist[c]={ leader:camps[c].leader, issued:0, claimed:0, used:0 }; } for (const t in tickets) { const parts=t.split("-"); const campname=parts[0]; const ticketnum=Number(parts[1]); if (tickets[t].status!="r") { camplist[campname].issued+=1; if (tickets[t].owner!="") camplist[campname].claimed+=1; if (tickets[t].status=="u") camplist[campname].used+=1; } } return res.render("camps",{ username:req.session.username, camps:camplist }); }) app.post("/camps",requireSuperUser,(req,res) => { console.log("New camp: "); const campname=req.body.campname; const leader=req.body.leader; const qty=Number(req.body.qty); camps[campname]??={ leader:leader, lastid:0 }; for (let i=0; i { const camplist={}; for (const t in tickets) { const parts=t.split("-"); const campname=parts[0]; const ticketnum=Number(parts[1]); camplist[campname]??={ issued:0, claimed:0, used:0 }; if (tickets[t].status!="r") { camplist[campname].issued+=1; if (tickets[t].owner!="") camplist[campname].claimed+=1; if (tickets[t].status=="u") camplist[campname].used+=1; } } return res.render("issue",{ camps:camplist }); }) app.get('/camplead', requireLogin, (req,res) => { let username=req.session.username; const edit={ tickets: {} }; for (const t in tickets) if (tickets[t].owner==username) { edit.tickets[t]={}; edit.tickets[t].owner=tickets[t].owner; edit.tickets[t].offered=tickets[t].offered; } return res.render("camplead",edit); }) app.get('/editcamp', requireSuperUser, (req,res) => { let campname=req.query.campname; const edit={ username:req.session.username, tickets: {} }; for (const t in tickets) { const parts=t.split("-"); const cname=parts[0]; const tnum=Number(parts[1]); if (cname==campname) { edit.tickets[t]={}; edit.tickets[t].owner=tickets[t].owner; edit.tickets[t].offered=tickets[t].offered; edit.tickets[t].status=tickets[t].status; } } return res.render("editcamp",edit); }) app.get('/manytickets', requireLogin, (req,res) => { let username=req.session.username; const edit={ username:req.session.username, tickets: {} }; for (const t in tickets) if (tickets[t].owner==username && tickets[t].status=="i") { edit.tickets[t]={}; edit.tickets[t].offered=tickets[t].offered; } return res.render("manytickets",edit); }) app.get('/mytickets',requireLogin, async (req,res)=> { let username=req.session.username; let claimed=0; let owned=0; let theticket=""; const edit={ username:req.session.username, tickets: {} }; for (const t in tickets) { if (tickets[t].status=="i" && tickets[t].offered==username) { claimed++; tickets[t].owner=username; tickets[t].offered=""; } if (tickets[t].status=="i" && tickets[t].owner==username) { owned++; if (owned==1) theticket=t; else theticket=""; edit.tickets[t]={}; edit.tickets[t].owner=tickets[t].owner; edit.tickets[t].offered=tickets[t].offered; } } if (owned==0) return res.render("message",{ username:username, message:"You have no unused tickets" }); else if (owned==1) { const hash0=crypto.createHash('sha256'); const hash1=hash0.update(theticket+QRSalt); const hash=hash1.digest("base64").slice(0,6); const dataURL=await QRCode.toDataURL('localhost:3000/useticket?t='+theticket+'&h='+hashQR(theticket,username)); return res.render("oneticket",{ username:username, ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL }); } else return res.render("manytickets",edit); }); app.post('/mytickets',requireLogin, async (req,res)=> { let username=req.session.username; let theticket=req.body.ticket; let offered=req.body.offered; if (tickets[theticket].owner==username && tickets[theticket].status=="i") { tickets[theticket].offered=offered; } const hash0=crypto.createHash('sha256'); const hash1=hash0.update(theticket+QRSalt); const hash=hash1.digest("base64").slice(0,6); const dataURL=await QRCode.toDataURL('localhost:3000/useticket?t='+theticket+'&h='+hashQR(theticket,username)); return res.render("oneticket", { username:username, ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL }); }); // 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 }; 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].status=="u"; } 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("/changestatus", requireSuperUser, (req,res) => { const ticket=req.body.ticket; console.log("Changestatus ",ticket); tickets[ticket].status=req.body.status; res.json({ message: 'Status received', status: req.body.status }); }) app.post("/updateoffered", requireLogin, (req,res) => { console.log("UpdateOffered being rubn"); const ticket=req.body.ticket; const offered=req.body.offered; if (tickets[ticket].owner!=req.session.username) res.status(500).send("Ticket "+ticket+" owned by someone else"); else if (tickets[ticket].status=="u") res.status(500).send("Ticket "+ticket+" has already been used"); else if (tickets[ticket].status=="r") res.status(500).send("Ticket "+ticket+" was revoked"); else { tickets[ticket].offered=offered; res.json({ message: 'Updated owner of '+ticket+' to '+offered }); } }) app.post("/updateticketsu", requireSuperUser, (req,res) => { const ticket=req.body.ticket; const owner=req.body.owner; const offered=req.body.offered; tickets[ticket].owner=req.body.owner; tickets[ticket].offered=req.body.offered; res.json({ message: 'Updated '+ticket }); }) 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].status!="i") res.status(500).send("Ticket "+ticket+" has already been used."); else { tickets[ticket].status="u"; res.send("

Welcome, "+tickets[ticket].owner+" 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) { if (!(tok in tokens)) return false; 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) => { console.log("In get /login: ",req.session); 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; console.log("In post /login",req.session); const redir=req.session.returnTo; delete req.session.returnTo; return res.redirect(redir || "/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; 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({ owner:tickets[ticket].owner, qrcode: URL }); }) app.post('/qrcodesu',requireSuperUser,async (req,res) => { const username=req.session.username; const ticket=req.body.ticket; const URL=await QRCode.toDataURL('localhost:3000/useticket?t='+ticket+'&h='+hashQR(ticket,username)); return res.send({ owner:tickets[ticket].owner, 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}`); });