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 %>!
-
+
+