diff --git a/foftickets.js b/foftickets.js index 8569bd1..81022ea 100644 --- a/foftickets.js +++ b/foftickets.js @@ -10,8 +10,9 @@ app.set('view engine','ejs'); app.use(express.json()); app.use(express.static('public')); const PORT = 3000; -const PWSalt="!SaltyMagic7283715374"; -const QRSalt="!SaltyMagic5392370662"; +const PWSalt ="!SaltyMagic7283715374"; +const EmailSalt="!SaltyMagic3562196239"; +const QRSalt ="!SaltyMagic5392370662"; // // ToDo, Actions: @@ -23,18 +24,41 @@ const QRSalt="!SaltyMagic5392370662"; // + Camp Editor // + Use Ticket // + Claim Any Tickets -// Purge Revoked Function (Admin) -// Signup -// Change Password -// Issue Tickets from Camp Admin Page +// + Purge Revoked Function (Admin) +// + Issue Tickets from Camp Admin Page +// See how much each camp/ticket has paid (Admin) +// Send email when new tickets are issued/offered // // ToDo, Later // + Use a templating engine // + Store password hashed and salted // Make all HTML look nice // Logging and Replay system +// Stripe Integration // +// +// Login workflow: +// There is always an implicit magic link that allows anyone to log in with their email address and hash(GlobalSalt+PerUserSalty+Email), except if the user sets a password for himself. +// We store a per-user salt, initially the empty string, in case we need to invalidate particular users' magic links. +// + +// +// Commands +// ISSUE campname email +// USE ticket +// STATUS ticket i/u/r +// CLAIM ticket email +// + +function hashEmail(email) { + const hash0=crypto.createHash('sha256'); + const usersalt=email in users ? (linksalt in users[email] ? users[email].linksalt : "") : ""; + const hash1=hash0.update(email+EmailSalt+usersalt); + const hash=hash1.digest("base64"); + return(hash); +} + function hashPW(pw) { const hash0=crypto.createHash('sha256'); const hash1=hash0.update(pw+PWSalt); @@ -54,8 +78,8 @@ function hashQR(t,ownername) { // // 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 users = { "teppy@egenesis.com" : { password: hashPW("x6321872X.1"), superuser:true, linksalt:"" }, + "fallsonfire@gmail.com" : { password: hashPW("indiantreeonfire"), superuser:false, linksalt:"boobsarefun" } }; // Status can be "i"=issued, "u"=used, "r"=revoked" @@ -69,11 +93,6 @@ const tickets = { "habitat-1" : { owner: "teppyx@egenesis.com" , offered: "", p 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({ @@ -92,7 +111,6 @@ function requireLogin(req, res, next) { 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'); } @@ -113,19 +131,10 @@ function generateSecureToken(length = 8) { 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

@@ -135,31 +144,6 @@ app.get('/emaillink',(req,res) => { ) }); -// 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={}; @@ -176,7 +160,7 @@ app.get('/camps',requireSuperUser, (req,res) => { if (tickets[t].status=="u") camplist[campname].used+=1; } } - return res.render("camps",{ username:req.session.username, camps:camplist }); + return res.render("camps",{ username:req.session.username, superuser:req.session.superuser, camps:camplist }); }) app.post("/camps",requireSuperUser,(req,res) => { @@ -186,37 +170,21 @@ app.post("/camps",requireSuperUser,(req,res) => { 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) { + const edit={ username:username, superuser:req.session.superuser, tickets: {} }; + for (const t in tickets) if (tickets[t].owner==username && tickets[t].status!='r') { 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("camplead",edit); }) @@ -224,7 +192,7 @@ app.get('/camplead', requireLogin, (req,res) => { app.get('/editcamp', requireSuperUser, (req,res) => { let campname=req.query.campname; - const edit={ username:req.session.username, tickets: {} }; + const edit={ username:req.session.username, superuser:req.session.superuser, campname:campname, leader:camps[campname].leader, tickets: {} }; for (const t in tickets) { const parts=t.split("-"); const cname=parts[0]; @@ -239,9 +207,22 @@ app.get('/editcamp', requireSuperUser, (req,res) => { return res.render("editcamp",edit); }) +app.post('/moretickets', requireSuperUser, (req,res) => { + const qty=Number(req.body.qty); + const campname=req.body.campname; + console.log("More tickets! ",campname in camps,qty); + if (campname in camps) for (let i=0; i { let username=req.session.username; - const edit={ username:req.session.username, tickets: {} }; + const edit={ username:req.session.username, superuser:req.session.superuser, 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; @@ -251,13 +232,14 @@ app.get('/manytickets', requireLogin, (req,res) => { app.get('/mytickets',requireLogin, async (req,res)=> { + console.log("In mytickets req.session is ",req.session); let username=req.session.username; let claimed=0; let owned=0; let theticket=""; - const edit={ username:req.session.username, tickets: {} }; + const edit={ username:req.session.username, superuser:req.session.superuser, 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].offered==username) { claimed++; tickets[t].owner=username; tickets[t].offered=""; } // LOG if (tickets[t].status=="i" && tickets[t].owner==username) { owned++; if (owned==1) theticket=t; else theticket=""; @@ -266,13 +248,13 @@ app.get('/mytickets',requireLogin, async (req,res)=> { edit.tickets[t].offered=tickets[t].offered; } } - if (owned==0) return res.render("message",{ username:username, message:"You have no unused tickets" }); + if (owned==0) return res.render("message",{ username:username, superuser:req.session.superuser, 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 }); + return res.render("oneticket",{ username:username, superuser:req.session.superuser, ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL }); } else return res.render("manytickets",edit); }); @@ -282,60 +264,21 @@ app.post('/mytickets',requireLogin, async (req,res)=> { let theticket=req.body.ticket; let offered=req.body.offered; if (tickets[theticket].owner==username && tickets[theticket].status=="i") { - tickets[theticket].offered=offered; + tickets[theticket].offered=offered; // LOG } 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 }); + return res.render("oneticket", { username:username, superuser:req.session.superuser, 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; + tickets[ticket].status=req.body.status; // LOG res.json({ message: 'Status received', status: req.body.status }); }) @@ -347,7 +290,7 @@ app.post("/updateoffered", requireLogin, (req,res) => { 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; + tickets[ticket].offered=offered; // LOG res.json({ message: 'Updated owner of '+ticket+' to '+offered }); } }) @@ -357,8 +300,8 @@ 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; + tickets[ticket].owner=req.body.owner; // LOG + tickets[ticket].offered=req.body.offered; // LOG res.json({ message: 'Updated '+ticket }); }) @@ -369,7 +312,7 @@ app.get("/useticket",(req,res) => { 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"; + tickets[ticket].status="u"; // LOG res.send("

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

"); } }) @@ -422,20 +365,12 @@ app.get('/testemail', // 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 - `); - } + if (req.session.username) return res.redirect("/mytickets"); + return res.render("login", { superuser:false }); +}); + +app.get('/login',(req,res) => { + return res.render("login",{ superuser:false }); }); app.get('/signup', (req, res) => { @@ -451,24 +386,6 @@ app.get('/signup', (req, res) => { }); -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) => { @@ -502,21 +419,9 @@ app.post('/changepassword', (req, res) => { return res.send('Passwords do not matchBack'); } users[req.session.username].password=hashPW(password1); - res.redirect('/login'); + res.redirect('/'); }); -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; @@ -526,7 +431,7 @@ app.post('/login', (req, res) => { console.log("In post /login",req.session); const redir=req.session.returnTo; delete req.session.returnTo; - return res.redirect(redir || "/transfer"); + return res.redirect(redir || "/mytickets"); } res.send('Invalid username or password. Try again'); }); @@ -552,28 +457,23 @@ app.post('/qrcodesu',requireSuperUser,async (req,res) => { 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('/settings',requireSuperUser, (req,res) => { + res.render('settings',{ username:req.session.username, superuser:req.session.superuser, message: "" }) + }); -app.get('/tickets', requireLogin, (req, res) => { - res.write(`

Tickets for `+req.session.username+`

`); - res.write(`Home`); - res.end(); -}); +app.post('/wipedb',requireSuperUser, (req,res) => { + for (const key in users ) delete users[key]; + for (const key in tickets) delete tickets[key]; + for (const key in camps ) delete camps[key]; + res.render('settings',{ username:req.session.username, superuser:req.session.superuser, message: "Wiped the database, but not the logfile." }) + }); + +app.post('/purge',requireSuperUser, (req,res) => { + let count=0; + for (const t in tickets) if (tickets[t].status=='r') { count++; delete tickets[t]; } + res.render('message',{ username:req.session.username, superuser:req.session.superuser, message: "Purged "+count+" revoked tickets" }) + }); -app.get('/faq', requireLogin, (req, res) => { - res.send(` -

FAQ Page

-

Here are some frequently asked questions.

- Home - `); -}); // Start the server app.listen(PORT, () => { diff --git a/views/camplead.ejs b/views/camplead.ejs index 3abda6a..3cba36b 100644 --- a/views/camplead.ejs +++ b/views/camplead.ejs @@ -12,28 +12,31 @@ -

Welcome, <%= username %>!

-
-
Server Ready
- - - - - - - - <% for (const t in tickets) { %> +
+

Welcome, <%= username %>!

+ +
Server Ready
+
Ticket#OwnerOffered ToAction
+ + + + + + + <% for (const t in tickets) { %> - <% } %> - - -
Ticket#OwnerOffered ToAction
<%=t%> <%=tickets[t].owner%>
-
+ <% } %> + + + + + +