This commit is contained in:
2024-12-28 19:31:45 -05:00
parent 69db561a0c
commit 1503af0e28
6 changed files with 180 additions and 172 deletions

View File

@@ -10,6 +10,7 @@ app.set('view engine','ejs');
app.use(express.json());
app.use(express.static('public'));
const PORT = 3000;
const MainURL ="http://localhost:3000";
const PWSalt ="!SaltyMagic7283715374";
const EmailSalt="!SaltyMagic3562196239";
const QRSalt ="!SaltyMagic5392370662";
@@ -26,8 +27,13 @@ const QRSalt ="!SaltyMagic5392370662";
// + Claim Any Tickets
// + Purge Revoked Function (Admin)
// + Issue Tickets from Camp Admin Page
// See how much each camp/ticket has paid (Admin)
// Make one Update button on manytickets (User)
// Make one Update button on editcamp (Admin)
// Send email when new tickets are issued/offered
// Magic-link Login System
// Deactivate individual magic links (User)
// Deactivate individual magic links (Admin)
// See how much each camp/ticket has paid (Admin)
//
// ToDo, Later
// + Use a templating engine
@@ -53,7 +59,7 @@ const QRSalt ="!SaltyMagic5392370662";
function hashEmail(email) {
const hash0=crypto.createHash('sha256');
const usersalt=email in users ? (linksalt in users[email] ? users[email].linksalt : "") : "";
const usersalt=email in users ? (users[email].linksalt ? users[email].linksalt : "") : "";
const hash1=hash0.update(email+EmailSalt+usersalt);
const hash=hash1.digest("base64");
return(hash);
@@ -73,25 +79,46 @@ function hashQR(t,ownername) {
return(hash);
}
function GetMagicLink(email) {
return MainURL+"/login?u="+email+"&h="+hashEmail(email);
}
function MagicLinkValid(email,hash) {
if (HasPW(email)) return false;
return hashEmail(email)==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, linksalt:"" },
"fallsonfire@gmail.com" : { password: hashPW("indiantreeonfire"), superuser:false, linksalt:"boobsarefun" }
};
//let 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"
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 tickets = { "habitat-1" : { owner: "teppy@egenesis.com" , offered: "" , paid: 0.00, status:"i" },
// "habitat-2" : { owner: "teppy@egenesis.com" , offered: "" , paid: 0.00, status:"u" },
// "habitat-3" : { owner: "ginachen94@gmail.com", offered: "teppy@egenesis.com" , paid: 0.00, status:"i" },
// "habitat-4" : { owner: "teppy@egenesis.com" , offered: "noahstern00@gmail.com", paid: 0.00, status:"i" },
// "habitat-5" : { owner: "teppy@egenesis.com" , offered: "" , paid: 0.00, status:"r" },
// "habitat-6" : { owner: "teppy@egenesis.com" , offered: "" , paid: 0.00, status:"i" },
// };
// const camps = { "habitat": { leader: "teppy@egenesis.com", lastid:6 } };
const camps = { "habitat": { leader: "teppy@egenesis.com", lastid:6 } };
const users={};
const tickets={};
const camps={};
function InitDatabase() {
for (const key in users ) delete users[key];
for (const key in tickets) delete tickets[key];
for (const key in camps ) delete camps[key];
users["teppy@egenesis.com" ]= { password: hashPW("x6321872X.1"), superuser:true, linksalt:"" };
users["fallsonfire@gmail.com"]= { password: hashPW("indiantreeonfire"), superuser:false, linksalt:"" };
}
InitDatabase();
// Middleware setup
app.use(bodyParser.urlencoded({ extended: true }));
@@ -164,7 +191,6 @@ app.get('/camps',requireSuperUser, (req,res) => {
})
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);
@@ -173,23 +199,11 @@ app.post("/camps",requireSuperUser,(req,res) => {
camps[campname].lastid+=1; // LOG
tickets[campname+'-'+camps[campname].lastid]={ owner: "", offered: leader, status:"i" }; // LOG
}
EmailTickets(leader);
return res.redirect("/camps");
})
app.get('/camplead', requireLogin, (req,res) => {
let username=req.session.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);
})
app.get('/editcamp', requireSuperUser, (req,res) => {
let campname=req.query.campname;
const edit={ username:req.session.username, superuser:req.session.superuser, campname:campname, leader:camps[campname].leader, tickets: {} };
@@ -210,7 +224,6 @@ app.get('/editcamp', requireSuperUser, (req,res) => {
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<qty; i++) {
camps[campname].lastid++;
tickets[campname+'-'+camps[campname].lastid]={ owner:"", offered:camps[campname].leader, paid:0, status:"i" };
@@ -232,7 +245,6 @@ 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;
@@ -247,43 +259,46 @@ app.get('/mytickets',requireLogin, async (req,res)=> {
edit.tickets[t].owner=tickets[t].owner;
edit.tickets[t].offered=tickets[t].offered;
}
}
if (owned==0) return res.render("message",{ username:username, superuser:req.session.superuser, message:"You have no unused tickets" });
}
let message="";
if (claimed>0) message="You have claimed "+claimed+" tickets.";
edit.message=message;
if (owned==0) return res.render("zerotickets",{ username:username, superuser:req.session.superuser, message:message });
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, superuser:req.session.superuser, ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL });
const useurl='/useticket?t='+theticket+'&h='+hashQR(theticket,username);
const dataURL=await QRCode.toDataURL(useurl);
return res.render("oneticket",{ username:username, superuser:req.session.superuser, message:message, ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL, useurl:useurl });
}
else return res.render("manytickets",edit);
});
app.post('/mytickets',requireLogin, async (req,res)=> {
app.post('/oneticket',requireLogin, async (req,res)=> {
let username=req.session.username;
let theticket=req.body.ticket;
let offered=req.body.offered;
let message="";
if (tickets[theticket].owner==username && tickets[theticket].status=="i") {
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, superuser:req.session.superuser, ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL });
const dataURL=await QRCode.toDataURL('/useticket?t='+theticket+'&h='+hashQR(theticket,username));
return res.render("oneticket", { username:username, superuser:req.session.superuser, message:message, magiclink:GetMagicLink(username), ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL });
});
app.post("/changestatus", requireSuperUser, (req,res) => {
const ticket=req.body.ticket;
console.log("Changestatus ",ticket);
tickets[ticket].status=req.body.status; // LOG
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");
@@ -295,6 +310,20 @@ app.post("/updateoffered", requireLogin, (req,res) => {
}
})
app.post("/updateoffered2", requireLogin, (req,res) => {
const changes={};
for (ticket in req.body) if (tickets[ticket] && tickets[ticket].owner==req.session.username && req.body[ticket]!=tickets[ticket].offered) {
if (!(req.body[ticket] in changes)) changes[req.body[ticket]]=0;
changes[req.body[ticket]]++;
}
// Ok to bail here if we need to
for (ticket in req.body) if (tickets[ticket] && tickets[ticket].owner==req.session.username && req.body[ticket]!=tickets[ticket].offered) {
tickets[ticket].offered=req.body[ticket];
}
for (email in changes) EmailTickets(email);
return res.redirect("manytickets");
})
app.post("/updateticketsu", requireSuperUser, (req,res) => {
const ticket=req.body.ticket;
@@ -307,43 +336,30 @@ app.post("/updateticketsu", requireSuperUser, (req,res) => {
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"; // LOG
res.send("<h1>Welcome, "+tickets[ticket].owner+" to Falls on Fire! Ticket "+ticket+" has now been used.</h1>");
}
let ticket=req.query.t;
let hash=req.query.h;
if (!tickets[ticket]) return res.status(500).send("Ticket "+ticket+" not found.");
if (hashQR(ticket,tickets[ticket].owner)!=hash) return res.status(500).send("Ticket "+ticket+" was transferred to "+tickets[ticket].owner);
if (tickets[ticket].status!="i") return res.status(500).send("Ticket "+ticket+" has already been used.");
tickets[ticket].status="u"; // LOG
return res.send("<h1>Welcome, "+tickets[ticket].owner+" to Falls on Fire! Ticket "+ticket+" has now been used.</h1>");
})
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: '<p>Your Login Link: </p>',
}).then(() => {
console.log('Email sent');
res.send("<h1>Email sent</h1>");
}).catch((error) => {
console.error('Error sending email:', error);
res.send("<h1>Email failed</h1>");
});
res.send("Email link sent");
})
async function EmailTickets(email) {
let offered=0;
for (const ticket in tickets) if (tickets[ticket].offered==email) offered++;
if (offered==0) return;
const textbody="You have been offered "+offered+" tickets to Falls On Fire! To claim them, visit this link:\n"+GetMagicLink(email);
const htmlbody="You have been offered "+offered+" tickets to Falls On Fire! To claim them, <a href=\""+GetMagicLink(email)+"\">click here.</a>";
await client.sendEmail({ From: "tickets@fallsonfire.net",
To: email,
Subject: "Falls on Fire: You've Got Tickets!",
TextBody: textbody,
HTMLBody: htmlbody
});
}
app.get('/testemail',
(req,res) => {
@@ -369,10 +385,42 @@ app.get('/', (req, res) => {
return res.render("login", { superuser:false });
});
function HasPW(username) {
if (!(username in users)) return false;
if (!users[username]) return false;
if (!users[username].password) return false;
if (users[username].password==0) return false;
if (users[username].password=="") return false;
return true;
}
app.get('/login',(req,res) => {
return res.render("login",{ superuser:false });
const username=req.query.u;
const hash=req.query.h;
if (MagicLinkValid(req.query.u,req.query.h)) return res.redirect("/mytickets");
return res.render("login",{ message: "Normal Login Required." });
});
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;
const redir=req.session.returnTo;
delete req.session.returnTo;
return res.redirect(redir || "/mytickets");
}
res.send('Invalid username or password. <a href="/login">Try again</a>');
});
app.get('/logout', (req, res) => {
req.session.destroy(() => {
res.redirect('/');
});
});
app.get('/signup', (req, res) => {
res.send(`
<h1>Sign Up</h1>
@@ -423,55 +471,35 @@ app.post('/changepassword', (req, res) => {
});
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 || "/mytickets");
}
res.send('Invalid username or password. <a href="/login">Try again</a>');
});
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 });
const URL=await QRCode.toDataURL('/useticket?t='+ticket+'&h='+hashQR(ticket,username));
return res.send({ owner:username, qrcode: URL, magiclink:GetMagicLink(username) });
})
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 });
const username=tickets[ticket].owner;
const URL=await QRCode.toDataURL('/useticket?t='+ticket+'&h='+hashQR(ticket,username));
return res.send({ owner:username, qrcode: URL, magiclink:GetMagicLink(username) });
})
app.get('/settings',requireSuperUser, (req,res) => {
res.render('settings',{ username:req.session.username, superuser:req.session.superuser, message: "" })
});
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];
InitDatabase();
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" })
res.render('settings',{ username:req.session.username, superuser:req.session.superuser, message: "Purged "+count+" revoked tickets" })
});

View File

@@ -24,7 +24,6 @@
.qrcode-image {
object-fit: contain;
grid-column:2;
}
/* Close button */
@@ -36,63 +35,30 @@
cursor: pointer;
}
.grid {
}
/* General Reset */
body {
margin: 0;
font-family: Arial, sans-serif;
display: grid;
grid-template-columns: 1fr 3fr;
height: 100vh; /* Full viewport height */
grid-template-columns: 240px auto;
grid-template-rows: 40px auto;
font-family: Arial, sans-serif;
}
/* Sidebar (Nav Links) */
.nav-links {
background-color: #f4f4f4; /* Light gray background */
padding: 20px;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); /* Add a slight shadow for separation */
.nav {
grid-column: 1;
min-height: 100vh; /* Stretch to full viewport height */
grid-row: 1 / 3;
background-color: #FFC0C0;
}
.nav-links nav ul {
list-style: none; /* Remove bullet points */
padding: 0;
}
.nav-links nav ul li {
margin: 10px 0; /* Add spacing between links */
}
.nav-links nav ul li a {
text-decoration: none; /* Remove underline */
color: #333;
}
.nav-links nav ul li a:hover {
color: #007BFF; /* Change link color on hover */
.message {
grid-column: 2;
grid-row: 1;
background-color: #C0FFC0;
align-content: center;
justify-items: center;
}
.content {
grid-column: 2;
flex-grow: 1; /* Allow content to fill the remaining space */
padding: 20px;
grid-row: 2;
background-color: #C0C0FF;
}
.menu-icon {
cursor: pointer;
display: inline-block;
}
.menu-icon span {
display: block;
width: 25px;
height: 3px;
margin: 5px 0;
background: #fff;
transition: 0.4s;
}

View File

@@ -13,7 +13,7 @@
</div>
</div>
<div class="content">
<form id="editor">
<form id="editor" method="POST" action="/updateoffered2">
<div id="server-response">Server Ready</div>
Tickets owned by <%=username%>
<table border="1">
@@ -25,13 +25,14 @@
<% for (const t in tickets) { %>
<tr>
<td><%=t%></td>
<td><input type="edit" value="<%=tickets[t].offered%>" id="<%=t%>-offered"></td>
<td><input type="text" class="offered" value="<%=tickets[t].offered%>" name="<%=t%>"></td>
<td><button id="<%=t%>-action" type="button"> QRCode </button></td>
</tr>
<% } %>
<tr>
</tr>
</table>
<button id="Update" type="submit">Update Offered</button>
</form>
</div>
<script>
@@ -96,13 +97,10 @@ document.body.addEventListener("click", event => {
const offereds = document.querySelectorAll("[id$=-offered]");
offereds.forEach(el => {
const id0=el.id.slice(0,-8);
const OfferedEdit = document.getElementById(id0+"-offered");
const ActionButton = document.getElementById(id0+"-action" );
OfferedEdit.addEventListener('input', () => { ActionButton.textContent = "Update"; ooEdits[id0]=true; console.log("Changed OfferedEdit:",OfferedEdit.value); });
});
const offereds = document.getElementsByClassName("offered");
const MessageArea=document.getElementById("message");
console.log("Offereds is ",offereds[0]);
for (let i=0; i<offereds.length; i++) offereds[i].addEventListener('input',(event)=>MessageArea.textContent= "Be sure to use the Update Offered button.");
</script>
</body>

View File

@@ -8,8 +8,9 @@
<%- include('partials/nav') %>
<div class="content">
To use <%=ticket%>, scan this QR Code:<br>
<img class="qrcode-image" width=300 height=300 src="<%=qrcode%>" alt="QR Code">
<form id="editor" method="POST">
<img class="qrcode-image" width=300 height=300 src="<%=qrcode%>" alt="QR Code"><br>
Or visit this URL: "<%=useurl%>"
<form id="editor" method="POST" action="/oneticket">
Or transfer <%=ticket%> to:<br>
<input type="hidden" name="ticket" value="<%=ticket%>">
<input type="email" placeholder="yourfriend@xyz.com" value="<%=offered%>" name="offered">

View File

@@ -1,14 +1,15 @@
<!-- views/partials/nav.ejs -->
<aside>
<nav class="nav-links">
Falls on Fire
<ul>
<li><a href="/mytickets">View My Tickets</a></li>
<%if (superuser) {%>
<li><a href="/camps">View Camps (Admin)</a></li>
<li><a href="/settings">Settings (Admin)</a></li>
<div class="nav">
Falls on Fire<br>
<a href="/mytickets">View My Tickets</a><br>
<%if (typeof superuser!='undefined' && superuser) {%>
<a href="/camps">View Camps (Admin)</a><br>
<a href="/settings">Settings (Admin)</a><br>
<%}%>
<li><a href="/logout">Log Out</a></li>
</ul>
</nav>
</aside>
<a href="/logout">Log Out</a><br>
</div>
<div class="message" id="message">
<% if (typeof message !== 'undefined') { %>
<p><%= message %></p>
<% } %>
</div>

14
views/zerotickets.ejs Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>No Tickets</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<%- include('partials/nav') %>
<div class="content">
You don't have any tickets assigned to you.
</div>
</body>
</html>