Files
FOFTickets/foftickets.js

838 lines
33 KiB
JavaScript
Raw Normal View History

2024-12-05 13:05:35 -05:00
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const cookieParser = require('cookie-parser');
2024-12-05 13:05:35 -05:00
const QRCode=require('qrcode');
const crypto=require('crypto');
2024-12-09 15:11:22 -05:00
const path=require('path');
2024-12-29 18:31:06 -05:00
const fs = require('fs');
2025-03-03 21:36:38 -05:00
const multer = require("multer");
const upload = multer();
const csvParse = require("csv-parse");
2025-02-08 23:22:19 -05:00
require('dotenv').config();
const port=process.env.PORT||3000;
const base_url = process.env.BASE_URL;
const stripe=require('stripe')(process.env.STRIPE_SECRET_KEY);
2024-12-05 13:05:35 -05:00
2025-03-03 21:36:38 -05:00
2024-12-05 13:05:35 -05:00
const app = express();
app.set('view engine','ejs');
app.use(express.json());
2024-12-19 15:57:33 -05:00
app.use(express.static('public'));
app.use(cookieParser());
2025-03-03 21:36:38 -05:00
app.use(session({
secret: 'supersecretkey',
resave: false,
saveUninitialized: false,
}));
2024-12-05 13:05:35 -05:00
const PORT = 3000;
2024-12-28 19:31:45 -05:00
const MainURL ="http://localhost:3000";
2024-12-28 00:07:12 -05:00
const PWSalt ="!SaltyMagic7283715374";
const EmailSalt="!SaltyMagic3562196239";
const QRSalt ="!SaltyMagic5392370662";
2024-12-05 13:05:35 -05:00
//
// ToDo, Actions:
// + Login with Password
// + Login with Token
// + Email Link
2024-12-27 08:49:34 -05:00
// + Display Many Tickets (User)
// + Display One Ticket (User)
// + Camp Editor
2024-12-05 13:05:35 -05:00
// + Use Ticket
2024-12-27 08:49:34 -05:00
// + Claim Any Tickets
2024-12-28 00:07:12 -05:00
// + Purge Revoked Function (Admin)
// + Issue Tickets from Camp Admin Page
2024-12-29 17:20:34 -05:00
// + Send email when new tickets are issued/offered
// + Make one Update button on editcamp (Admin)
// + Make one Update button on manytickets (User)
2024-12-29 18:31:06 -05:00
// + Turn ticket use on/off from Settings (Admin)
// + Turn email on/off from Settings (Admin)
// + Magic-link Login System
2025-03-03 23:53:35 -05:00
// + Convert all the routes to use common.(user,superuser,etc)
// + Display messages for all GET routes?
// + Setting to deactivate transfers globally
// + Mass-import of individual tickets
2025-02-21 20:54:53 -05:00
// + Cookie based QR code functionality
// + Create Account (User)
// + Change Password (User)
2025-03-03 23:53:35 -05:00
// + Change most POST routes to end in redirect instead of render.
2025-03-09 22:13:57 -04:00
// + See how much each camp/ticket has paid (Admin)
// + Purchase individual open camping tickets
// + Deactivate individual magic links (Admin)
2025-03-13 20:41:35 -04:00
// + Get it working again online
// Make all HTML look nice
2025-03-12 22:40:41 -04:00
// If you only have a magic link, supress "Change Password"
// Once a ticket is used, disallow transfer (oneticket)
// Once a ticket is used, disallow transfer (manytickets)
// Why does the live version redirect back to localhost?
// Make sure open camping tickets actually send email
// Don't hardcode MainURL and PORT
// Limit number of Open Camping tickets
2025-03-13 23:01:20 -04:00
// + Make sure subsequent FB imports don't add duplicate tickets
2025-03-03 23:53:35 -05:00
// Maybe:
// Deactivate individual magic links (User)
// Option to "Email me my QR Code"
2024-12-05 13:05:35 -05:00
//
// ToDo, Later
2024-12-19 15:57:33 -05:00
// + Use a templating engine
// + Store password hashed and salted
2025-03-09 22:13:57 -04:00
// + Stripe Integration
2024-12-29 17:20:34 -05:00
// Logging and Replay system(?)
// More efficent data structure: TicketsByCamp, TicketsByOffered, TicketsByOwner
2024-12-05 13:05:35 -05:00
//
2024-12-28 00:07:12 -05:00
//
// 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
//
2025-02-08 23:22:19 -05:00
//
// Stripe Integration:
// The Stripe CLI is configured for Andrew Tepper with account id acct_1QZlMSCHHjDgpHos
// Login on stripe.com is tickets@fallsonfire.net Password is x65195241X.1
//
2024-12-29 18:06:39 -05:00
function base64ToBase64Url(base64) {
return base64
.replace(/\+/g, '-') // Replace '+' with '-'
.replace(/\//g, '_') // Replace '/' with '_'
.replace(/=+$/, ''); // Remove trailing '='
}
2024-12-28 00:07:12 -05:00
function hashEmail(email) {
const hash0=crypto.createHash('sha256');
2024-12-28 19:31:45 -05:00
const usersalt=email in users ? (users[email].linksalt ? users[email].linksalt : "") : "";
2024-12-28 00:07:12 -05:00
const hash1=hash0.update(email+EmailSalt+usersalt);
const hash=hash1.digest("base64");
2024-12-29 18:06:39 -05:00
return base64ToBase64Url(hash);
2024-12-28 00:07:12 -05:00
}
2024-12-05 13:05:35 -05:00
function hashPW(pw) {
const hash0=crypto.createHash('sha256');
const hash1=hash0.update(pw+PWSalt);
const hash=hash1.digest("base64");
return(hash);
}
2024-12-14 23:26:38 -05:00
function hashQR(t,ownername) {
2024-12-05 13:05:35 -05:00
const hash0=crypto.createHash('sha256');
2024-12-14 23:26:38 -05:00
const hash1=hash0.update(t+QRSalt+ownername);
2024-12-29 18:06:39 -05:00
const hash=base64ToBase64Url(hash1.digest("base64")).slice(0,6);
2024-12-05 13:05:35 -05:00
return(hash);
}
2024-12-28 19:31:45 -05:00
function GetMagicLink(email) {
return MainURL+"/login?u="+email+"&h="+hashEmail(email);
}
function MagicLinkValid(email,hash) {
if (HasPW(email)) return false;
2024-12-29 18:06:39 -05:00
return hash==hashEmail(email);
2024-12-28 19:31:45 -05:00
}
2024-12-29 18:06:39 -05:00
2025-02-25 00:02:58 -05:00
app.use((req, res, next) => {
res.locals.commonData = {
2025-03-03 23:53:35 -05:00
username: req.username, // Attach user info if available
2025-02-25 00:02:58 -05:00
superuser: req.superuser,
2025-03-03 23:53:35 -05:00
settings: settings,
error: req.session && req.session.error || null, // Flash error messages
message: req.session && req.session.message || null, // Flash success messages
2025-02-25 00:02:58 -05:00
};
// Clear session-based flash messages after use
if (req.session) {
delete req.session.error;
2025-03-03 21:36:38 -05:00
delete req.session.message;
2025-03-04 21:34:18 -05:00
}
2025-02-25 00:02:58 -05:00
next();
});
2025-03-04 21:34:18 -05:00
2024-12-05 13:05:35 -05:00
//
// In-memory data structures
//
// There are two ways to log in: with a username/password or with a token.
//
2024-12-28 19:31:45 -05:00
//let users = { "teppy@egenesis.com" : { password: hashPW("x6321872X.1"), superuser:true, linksalt:"" },
// "fallsonfire@gmail.com" : { password: hashPW("indiantreeonfire"), superuser:false, linksalt:"boobsarefun" }
// };
//
2024-12-26 00:33:23 -05:00
// Status can be "i"=issued, "u"=used, "r"=revoked"
2024-12-28 19:31:45 -05:00
// 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 } };
2024-12-29 18:31:06 -05:00
let users={};
let tickets={};
let camps={};
2025-03-13 23:01:20 -04:00
let settings={ "enable-transfer":true, "open-limit":0 };
2024-12-28 19:31:45 -05:00
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];
2024-12-29 20:05:39 -05:00
users["teppy@egenesis.com" ]= { password: hashPW("x6321872X.1"), superuser:true, linksalt:"" };
users["ginachen94@gmail.com" ]= { password: hashPW("indiantreeonfire"),superuser:true, linksalt:"" };
2025-03-09 22:13:57 -04:00
camps["open"]={ leader:"", lastid:0 };
2024-12-28 19:31:45 -05:00
}
InitDatabase();
2024-12-09 15:11:22 -05:00
2024-12-29 18:31:06 -05:00
function SerializeAll() {
const tables={ users, tickets, camps, settings };
fs.writeFileSync('foftickets.json', JSON.stringify(tables, null, 2), 'utf8');
}
function DeserializeAll() {
const data = fs.readFileSync('foftickets.json', 'utf8');
const tables = JSON.parse(data);
users=tables.users;
tickets=tables.tickets;
camps=tables.camps;
settings=tables.settings;
}
2024-12-05 13:05:35 -05:00
// Middleware setup
app.use(bodyParser.urlencoded({ extended: true }));
// Middleware to protect routes
function requireLogin(req, res, next) {
2024-12-19 15:57:33 -05:00
if (req.session.username) return next();
2024-12-26 00:33:23 -05:00
req.session.returnTo=req.originalUrl;
2024-12-19 15:57:33 -05:00
return res.redirect('/login');
2024-12-09 15:11:22 -05:00
}
2024-12-05 13:05:35 -05:00
2024-12-09 15:11:22 -05:00
function requireSuperUser(req,res,next) {
2024-12-19 15:57:33 -05:00
if (req.session.superuser) return next();
req.session.returnTo=req.originalUrl;
return res.redirect('/login');
2024-12-09 15:11:22 -05:00
}
2024-12-05 13:05:35 -05:00
2024-12-09 15:11:22 -05:00
app.get('/styles.css', (req, res) => {
res.setHeader('Content-Type', 'text/css');
res.sendFile(path.join(__dirname, 'public', 'styles.css'));
});
2024-12-05 13:05:35 -05:00
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;
}
2024-12-28 00:07:12 -05:00
2024-12-05 13:05:35 -05:00
const postmark = require('postmark');
const client = new postmark.ServerClient('f45bcb9f-6556-4420-9e21-05d16739b5e8');
app.get('/emaillink',(req,res) => {
res.send(`
<h1>Email me a login link</h1>
<form method="POST" action="/emaillink">
<label>Email Address: <input name="email"></label>
<button type="submit">Submit</button>`
)
2024-12-19 15:57:33 -05:00
});
2024-12-05 13:05:35 -05:00
2024-12-21 00:29:45 -05:00
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, npaid:0, paid:0 };
2024-12-21 00:29:45 -05:00
}
for (const t in tickets) {
const parts=t.split("-");
2024-12-26 00:33:23 -05:00
const campname=parts[0];
2024-12-21 00:29:45 -05:00
const ticketnum=Number(parts[1]);
2024-12-26 00:33:23 -05:00
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;
if (tickets[t].paid>0) camplist[campname].npaid+=1;
camplist[campname].paid+=tickets[t].paid;
2024-12-26 00:33:23 -05:00
}
2024-12-21 00:29:45 -05:00
}
2024-12-28 00:07:12 -05:00
return res.render("camps",{ username:req.session.username, superuser:req.session.superuser, camps:camplist });
2024-12-21 00:29:45 -05:00
})
2025-03-09 22:13:57 -04:00
2024-12-21 00:29:45 -05:00
2024-12-26 00:33:23 -05:00
app.post("/camps",requireSuperUser,(req,res) => {
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<qty; i++) {
2024-12-28 00:07:12 -05:00
camps[campname].lastid+=1; // LOG
tickets[campname+'-'+camps[campname].lastid]={ owner: "", offered: leader, status:"i", paid:0 }; // LOG
2024-12-26 00:33:23 -05:00
}
2024-12-28 19:31:45 -05:00
EmailTickets(leader);
2024-12-26 00:33:23 -05:00
return res.redirect("/camps");
})
2024-12-21 00:29:45 -05:00
2024-12-26 00:33:23 -05:00
app.get('/editcamp', requireSuperUser, (req,res) => {
let campname=req.query.campname;
2024-12-29 17:20:34 -05:00
if (!camps[campname]) return res.redirect("/");
2024-12-28 00:07:12 -05:00
const edit={ username:req.session.username, superuser:req.session.superuser, campname:campname, leader:camps[campname].leader, tickets: {} };
2024-12-26 00:33:23 -05:00
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;
edit.tickets[t].paid=tickets[t].paid;
2024-12-26 00:33:23 -05:00
}
}
return res.render("editcamp",edit);
2024-12-19 15:57:33 -05:00
})
2024-12-26 00:33:23 -05:00
2024-12-28 00:07:12 -05:00
app.post('/moretickets', requireSuperUser, (req,res) => {
const qty=Number(req.body.qty);
const campname=req.body.campname;
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" };
}
2024-12-28 19:40:30 -05:00
EmailTickets(camps[campname].leader);
2024-12-28 00:07:12 -05:00
const referer = req.get('Referer');
if (referer) return res.redirect(referer);
else return res.redirect('/');
});
2024-12-26 21:46:13 -05:00
app.get('/manytickets', requireLogin, (req,res) => {
let username=req.session.username;
2025-03-03 23:53:35 -05:00
const edit={ username:req.session.username, superuser:req.session.superuser, tickets: {}, settings:settings };
2024-12-26 21:46:13 -05:00
for (const t in tickets) if (tickets[t].owner==username && tickets[t].status=="i") {
edit.tickets[t]={};
edit.tickets[t].offered=tickets[t].offered;
edit.tickets[t].paid=tickets[t].paid;
2024-12-26 21:46:13 -05:00
}
return res.render("manytickets",edit);
})
2024-12-26 00:33:23 -05:00
2025-03-09 23:12:06 -04:00
async function renderOneOrManyTickets(req,res) {
2024-12-26 00:33:23 -05:00
let username=req.session.username;
let claimed=0;
let owned=0;
let theticket="";
2025-03-03 23:53:35 -05:00
const edit={ username:req.session.username, superuser:req.session.superuser, tickets: {}, settings:settings };
2024-12-26 00:33:23 -05:00
for (const t in tickets) {
2024-12-28 00:07:12 -05:00
if (tickets[t].status=="i" && tickets[t].offered==username) { claimed++; tickets[t].owner=username; tickets[t].offered=""; } // LOG
2024-12-26 00:33:23 -05:00
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;
edit.tickets[t].paid=tickets[t].paid;
2024-12-26 00:33:23 -05:00
}
2024-12-28 19:31:45 -05:00
}
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 });
2024-12-26 21:46:13 -05:00
else if (owned==1) {
const hash0=crypto.createHash('sha256');
const hash1=hash0.update(theticket+QRSalt);
const hash=hash1.digest("base64").slice(0,6);
2024-12-28 19:45:07 -05:00
const useurl=MainURL+'/useticket?t='+theticket+'&h='+hashQR(theticket,username);
2024-12-28 19:31:45 -05:00
const dataURL=await QRCode.toDataURL(useurl);
2025-03-03 23:53:35 -05:00
const data={ username:username, superuser:req.session.superuser, message:message, magiclink:GetMagicLink(username),
ticket:theticket, offered:tickets[theticket].offered, paid:tickets[theticket].paid, qrcode:dataURL, useurl:useurl,
settings: settings }
return res.render("oneticket",data);
2024-12-26 21:46:13 -05:00
}
2024-12-26 00:33:23 -05:00
else return res.render("manytickets",edit);
2025-03-09 23:12:06 -04:00
}
app.get('/mytickets',requireLogin, renderOneOrManyTickets);
2024-12-19 15:57:33 -05:00
app.get("/oneticket",requireLogin, async (req,res) => {
2024-12-26 21:46:13 -05:00
let username=req.session.username;
let ticket=req.query.t;
2025-02-21 20:54:53 -05:00
if (!tickets[ticket]) return res.render("error",{ username:username, superuser:req.session.superuser, message: "Ticket not found: "+ticket });
if (req.session.superuser) username=tickets[ticket].owner;
else if (tickets[ticket].owner!=username) return res.render("error", { username:username, superuser:req.session.superuser, message: "You are not the owner of ticket "+ticket });
let offered=tickets[ticket].offered;
let message="";
const hash0=crypto.createHash('sha256');
const hash1=hash0.update(ticket+QRSalt);
const hash=hash1.digest("base64").slice(0,6);
const useurl=MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username);
const dataURL=await QRCode.toDataURL(MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username));
return res.render("oneticket", { username:username, superuser:req.session.superuser, message:message, magiclink:GetMagicLink(username),
2025-02-21 20:54:53 -05:00
ticket:ticket, offered:tickets[ticket].offered, paid:tickets[ticket].paid, qrcode:dataURL, useurl:useurl,
2025-03-03 23:53:35 -05:00
settings:settings });
});
2025-02-21 20:54:53 -05:00
app.post('/oneticket',requireLogin, async (req,res) => { // Make sure the ticket is owned by the logged in user!
let username=req.session.username;
let ticket=req.body.ticket;
if (!tickets[ticket] || tickets[ticket].owner!=username) return res.render("error", { username:username, superuser:req.session.superuser, message: "You are not the owner of ticket "+ticket });
2024-12-26 21:46:13 -05:00
let offered=req.body.offered;
2025-03-09 22:13:57 -04:00
console.log("Trying to transfer ",ticket);
2024-12-28 19:31:45 -05:00
let message="";
2025-02-21 20:54:53 -05:00
if (req.session.cantransfer && tickets[ticket].owner==username && tickets[ticket].status=="i") {
tickets[ticket].offered=offered; // LOG
2024-12-28 19:40:30 -05:00
EmailTickets(offered);
2024-12-26 21:46:13 -05:00
}
const hash0=crypto.createHash('sha256');
const hash1=hash0.update(ticket+QRSalt);
2024-12-26 21:46:13 -05:00
const hash=hash1.digest("base64").slice(0,6);
const dataURL=await QRCode.toDataURL(MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username));
const useurl=MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username);
2025-02-21 20:54:53 -05:00
return res.redirect("/");
2024-12-26 21:46:13 -05:00
});
2024-12-05 13:05:35 -05:00
2024-12-26 00:33:23 -05:00
app.post("/changestatus", requireSuperUser, (req,res) => {
const ticket=req.body.ticket;
2024-12-28 00:07:12 -05:00
tickets[ticket].status=req.body.status; // LOG
2024-12-26 00:33:23 -05:00
res.json({ message: 'Status received', status: req.body.status });
})
2024-12-28 19:31:45 -05:00
app.post("/updateoffered2", requireLogin, (req,res) => {
2025-03-03 23:53:35 -05:00
if (!settings["enable-transfer"]) {
return res.render("error",{ message: "Transfer functionality has been disabled by the admin." });
}
2024-12-28 19:31:45 -05:00
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);
2025-02-21 20:54:53 -05:00
return res.redirect("/manytickets");
2024-12-28 19:31:45 -05:00
})
2024-12-19 15:57:33 -05:00
2024-12-29 17:20:34 -05:00
app.post("/updateoffered2su", requireSuperUser, (req,res) => {
const emaillist={};
for (name in req.body) {
let ticket=0;
if (name.endsWith("-owner" )) { ticket=name.slice(0,-6); tickets[ticket].owner =req.body[name]; }
else if (name.endsWith("-status" )) { ticket=name.slice(0,-7); tickets[ticket].status =req.body[name]; }
else if (name.endsWith("-offered")) {
ticket=name.slice(0,-8);
if (tickets[ticket].offered!=req.body[name]) {
tickets[ticket].offered=req.body[name];
emaillist[req.body[name]]=1;
}
}
}
for (email in emaillist) EmailTickets(email);
const referer = req.get('Referer');
if (referer) return res.redirect(referer);
else return res.redirect('/');
})
2024-12-26 00:33:23 -05:00
app.post("/updateticketsu", requireSuperUser, (req,res) => {
const ticket=req.body.ticket;
const owner=req.body.owner;
const offered=req.body.offered;
2024-12-28 00:07:12 -05:00
tickets[ticket].owner=req.body.owner; // LOG
tickets[ticket].offered=req.body.offered; // LOG
2024-12-26 00:33:23 -05:00
res.json({ message: 'Updated '+ticket });
})
2024-12-05 13:05:35 -05:00
app.get("/useticket",(req,res) => {
2024-12-28 19:31:45 -05:00
let ticket=req.query.t;
let hash=req.query.h;
2025-02-21 20:54:53 -05:00
if (!tickets[ticket]) return res.render("error", { message: "Ticket "+ticket+" not found." });
if (tickets[ticket].status!="i") return res.render("error", { message: "Ticket "+ticket+" has already been used." });
if (hashQR(ticket,tickets[ticket].owner)!=hash) return res.render("error", { message: "Ticket "+ticket+" was transferred to "+tickets[ticket].owner });
let paid_message=tickets[ticket].paid;
2025-02-21 20:54:53 -05:00
if (tickets[ticket].paid==0) paid_message="<br>This ticket has not been paid for. Encourage a donation at the gate.";
if (!settings['enable-ticket-use'])
return res.render("message", { message: "Your ticket is good, "+tickets[ticket].owner+". The server is not in Event Mode, so Ticket "+ticket+" is still valid."+paid_message });
if (req.cookies["fof_scanqr"]!="on")
return res.render("message", { message: "Ticket "+ticket+" owned by "+tickets[ticket].owner+" is good, but scanning on this device has not been enabled."+paid_message });
tickets[ticket].status="u"; // LOG
return res.render("message", { message: "Welcome, "+tickets[ticket].owner+" to Falls on Fire! Ticket "+ticket+" has now been used."+paid_message });
2024-12-29 17:20:34 -05:00
})
2024-12-05 13:05:35 -05:00
2025-03-09 23:12:06 -04:00
async function EmailTickets_fof(email) {
2024-12-28 19:31:45 -05:00
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>";
2024-12-29 18:06:39 -05:00
if (!settings['enable-email']) {
console.log("Email disabled. Would have sent to "+email+": "+textbody);
return;
}
2024-12-28 19:31:45 -05:00
await client.sendEmail({ From: "tickets@fallsonfire.net",
To: email,
Subject: "Falls on Fire: You've Got Tickets!",
TextBody: textbody,
HTMLBody: htmlbody
});
}
2024-12-05 13:05:35 -05:00
2025-03-09 23:12:06 -04:00
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 the Frostburn Decompression! To claim them, visit this link:\n"+GetMagicLink(email);
const htmlbody="You have been offered "+offered+" tickets to the Frostburn Decompression! To claim them, <a href=\""+GetMagicLink(email)+"\">click here.</a>";
if (!settings['enable-email']) {
console.log("Email disabled. Would have sent to "+email+": "+textbody);
return;
}
await client.sendEmail({ From: "tickets@fallsonfire.net",
To: email,
Subject: "Frostburn Decompression: You've Got Tickets!",
TextBody: textbody,
HTMLBody: htmlbody
});
}
2024-12-05 13:05:35 -05:00
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: '<p>This is a test email.</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>");
})});
// Routes
app.get('/', (req, res) => {
2025-03-09 23:12:06 -04:00
if (req.session.username) return renderOneOrManyTickets(req,res);
2024-12-28 00:07:12 -05:00
return res.render("login", { superuser:false });
});
2024-12-28 19:31:45 -05:00
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;
}
2024-12-28 00:07:12 -05:00
app.get('/login',(req,res) => {
2024-12-28 19:31:45 -05:00
const username=req.query.u;
const hash=req.query.h;
2024-12-29 17:33:20 -05:00
if (MagicLinkValid(username,hash)) {
req.session.username=username;
return res.redirect("/mytickets");
}
2024-12-28 19:31:45 -05:00
return res.render("login",{ message: "Normal Login Required." });
2024-12-05 13:05:35 -05:00
});
2024-12-28 19:31:45 -05:00
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");
2025-02-25 00:02:58 -05:00
}
req.session.error="Invalid username or password.";
return res.redirect("/login");
2024-12-28 19:31:45 -05:00
});
app.get('/logout', (req, res) => {
req.session.destroy(() => {
res.redirect('/');
});
});
app.get('/create', (req, res) => {
return res.render("create");
2024-12-05 13:05:35 -05:00
});
app.post('/create', async (req, res) => {
const { username, password1, password2 } = req.body;
2025-03-09 23:12:06 -04:00
if (password1!=password2) { req.session.message="Passwords do not match."; return res.redirect('/'); }
if (users[username] && !users[username].needsconfirm) { req.session.error="Email (username) already exists."; return res.redirect('/'); }
if (users[username] && users[username].needsconfirm) {
await client.sendEmail({ From: "tickets@fallsonfire.net",
To: username,
2025-03-09 23:12:06 -04:00
Subject: "Confirm Account Creation",
TextBody: "Click here to confirm creation of account "+username,
HTMLBody: "Click here to confirm creation of account "+username
});
2025-03-09 23:12:06 -04:00
req.session.message="Email has not yet been confirmed. Resent confirm link.";
return res.redirect('/');
}
users[username] = { password: hashPW(password1), needsconfirm:false };
2025-03-09 23:12:06 -04:00
if (users[username].needsconfirm) req.session.message="Check email to confirm account creation.";
else req.session.message="Account created. You may now log in.";
return res.redirect('/');
});
2024-12-05 13:05:35 -05:00
app.get("/scanqron", (req,res) => {
res.cookie("fof_scanqr","on",{ maxAge: 7 * 24 * 60 * 60 * 1000 });
return res.redirect("/checkscanqr");
});
2024-12-05 13:05:35 -05:00
app.get("/scanqroff", (req,res) => {
res.cookie("fof_scanqr","off");
return res.redirect("/checkscanqr");
});
app.get("/checkscanqr", (req,res) => {
const scan=req.cookies["fof_scanqr"];
return res.render("message",{ message: "QR Code Scanning is "+(scan=="on" ? "On" : "Off") });
});
2024-12-05 13:05:35 -05:00
app.get('/changepassword', requireLogin,(req, res) => {
2025-02-25 00:02:58 -05:00
return res.render("changepassword",{ username:req.session.username, superuser:req.session.superuser, settings:settings, message: "" });
});
app.post('/changepassword', requireLogin,(req, res) => {
const { password0, password1, password2 } = req.body;
2025-03-09 23:12:06 -04:00
if (users[req.session.username].password!=hashPW(password0)) { req.session.error="Old Password is not correct."; return res.redirect('/changepassword'); }
if (password1!=password2) { req.session.error="Passwords do not match"; return res.redirect('/changepassword'); }
users[req.session.username].password=hashPW(password1);
2025-03-09 23:12:06 -04:00
req.session.message="Password Changed.";
return res.redirect('/');
2024-12-05 13:05:35 -05:00
});
2024-12-14 23:26:38 -05:00
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");
2024-12-28 19:45:07 -05:00
const URL=await QRCode.toDataURL(MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username));
2024-12-28 19:31:45 -05:00
return res.send({ owner:username, qrcode: URL, magiclink:GetMagicLink(username) });
2024-12-14 23:26:38 -05:00
})
2024-12-26 00:33:23 -05:00
app.post('/qrcodesu',requireSuperUser,async (req,res) => {
const ticket=req.body.ticket;
2024-12-28 19:31:45 -05:00
const username=tickets[ticket].owner;
2024-12-28 19:45:07 -05:00
const URL=await QRCode.toDataURL(MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username));
2024-12-28 19:31:45 -05:00
return res.send({ owner:username, qrcode: URL, magiclink:GetMagicLink(username) });
2024-12-26 00:33:23 -05:00
})
2025-03-13 23:01:20 -04:00
function opencount() {
let rval=0;
for (t in tickets) if (t.startsWith("open-")) if (tickets[t].status!='r') rval++;
return rval;
}
2024-12-28 00:07:12 -05:00
app.get('/settings',requireSuperUser, (req,res) => {
2025-03-13 23:01:20 -04:00
res.render('settings',{ username:req.session.username, superuser:req.session.superuser, settings:settings, opencount:opencount(), message: "" })
});
app.post('/addopen',requireSuperUser,(req,res) => {
settings['open-limit']+=Number(req.body.addopen);
return res.redirect('/settings');
2024-12-28 00:07:12 -05:00
});
2024-12-05 13:05:35 -05:00
2025-03-03 21:36:38 -05:00
app.post('/importfb',requireSuperUser,upload.single("file"),(req,res) => {
console.log("File name:", req.file.originalname);
2025-03-13 23:01:20 -04:00
let emails={};
for (t in tickets) {
if (tickets[t].offered) emails[tickets[t].offered]=1;
if (tickets[t].owner ) emails[tickets[t].owner ]=1;
}
2025-03-03 21:36:38 -05:00
const contents=req.file.buffer.toString();
csvParse.parse(contents, { columns: true, trim: true }, (err, records) => {
if (err) {
console.log("CSV Parsing Error:", err);
req.session.error="The CVS file did not parse correctly. Check console.";
return res.redirect("/settings");
}
2025-03-03 23:53:35 -05:00
let count=0;
2025-03-13 23:01:20 -04:00
for (const item of records) if (item.email in emails) {
2025-03-03 23:53:35 -05:00
if (!camps[item.camp]) camps[item.camp]={ leader:"", lastid:0 };
camps[item.camp].lastid++;
const ticket=item.camp+"-"+camps[item.camp].lastid;
tickets[ticket]={ owner:"", offered: item.email, paid:0.00, status:"i" };
console.log("Offered ",ticket," to ",item.email);
count++;
2025-03-13 23:01:20 -04:00
emails[item.email]=1;
2025-03-03 23:53:35 -05:00
}
req.session.message="Imported "+count+" Frostburn-style records.";
2025-03-03 21:36:38 -05:00
return res.redirect("/settings");
});
});
2024-12-28 00:07:12 -05:00
app.post('/wipedb',requireSuperUser, (req,res) => {
2024-12-28 19:31:45 -05:00
InitDatabase();
2025-02-21 20:54:53 -05:00
res.redirect("/");
2024-12-28 00:07:12 -05:00
});
2025-02-21 20:54:53 -05:00
app.post('/serialize',requireSuperUser, async (req,res) => {
2024-12-29 18:31:06 -05:00
SerializeAll();
2025-02-21 20:54:53 -05:00
return res.redirect("/settings");
2024-12-29 18:31:06 -05:00
});
app.post('/deserialize',requireSuperUser, (req,res) => {
DeserializeAll();
2025-03-13 23:01:20 -04:00
return res.redirect("/settings"); // Since we may be overwriting session
2024-12-29 18:31:06 -05:00
});
2024-12-28 00:07:12 -05:00
app.post('/purge',requireSuperUser, (req,res) => {
let count=0;
for (const t in tickets) if (tickets[t].status=='r') { count++; delete tickets[t]; }
2025-02-21 20:54:53 -05:00
return res.redirect("/settings");
2024-12-28 00:07:12 -05:00
});
2024-12-05 13:05:35 -05:00
2025-03-09 22:13:57 -04:00
app.post("/killmagiclink",requireSuperUser,(req,res) => {
if (!users[req.body.email]) users[req.body.email]={ };
users[req.body.email].linksalt=generateSecureToken();
req.session.message="Deactivated any previous Magic Links for "+req.body.email;
return res.redirect("/settings");
});
2024-12-29 17:20:34 -05:00
app.post('/update-setting', requireSuperUser, (req, res) => {
settings[req.body.name]=req.body.checked;
res.json({ success: true, message: 'Checkbox state updated successfully' });
2025-03-04 21:34:18 -05:00
});
2024-12-29 17:20:34 -05:00
app.post('/pay0',requireLogin,(req,res) => {
2025-03-09 22:13:57 -04:00
return res.render("pay",{ username:req.session.username,
superuser:req.session.superuser,
payforwhat: { kind:"existing", ticket:req.body.ticket, amount:+req.body.amount, total:+req.body.amount, qty:1 },
});
});
function check_payforwhat(payforwhat,req) {
if (payforwhat.kind=="existing") {
let ticket=tickets[payforwhat.ticket];
if (!ticket || ticket.status=="r" || ticket.owner!=req.session.username) return false;
}
else if (payforwhat.kind=="new") {
if (payforwhat.qty<=0 || payforwhat.qty>6 || payforwhat.amounteach<=0 || payforwhat.amounteach>100000) return false;
}
return true;
}
function assure_camp_exists(camp) {
if (!camps[camp]) camps[camp]={ leader:"", lastid:0 };
}
function do_payforwhat(payforwhat) {
if (payforwhat.kind=="existing") {
tickets[payforwhat.ticket].paid=100.0*payforwhat.amount;
}
else if (payforwhat.kind=="new") {
assure_camp_exists(payforwhat.camp);
for (let i=0; i<payforwhat.qty; i++) {
camps[payforwhat.camp].lastid++;
tickets[payforwhat.camp+"-"+camps[payforwhat.camp].lastid]={ owner:payforwhat.email, offered:"",paid:100.0*payforwhat.amounteach,status:"i" };
}
2025-03-13 23:01:20 -04:00
EmailTickets(payforwhat.email);
2025-03-09 22:13:57 -04:00
}
}
app.get("/buy",(req,res) => {
return res.render("buy",{ username:req.session.username, superuser:req.session.superuser, settings:settings, message: "" });
});
app.post('/buy',requireLogin,(req,res) => {
return res.render("pay",{ username:req.session.username,
superuser:req.session.superuser,
payforwhat: { kind:"new", camp:"open", email:req.body.email, qty:req.body.qty, amounteach:req.body.amounteach, total:req.body.qty*req.body.amounteach }});
});
2025-02-08 23:22:19 -05:00
2025-03-11 19:07:16 -04:00
app.post('/charge', requireLogin, async (req, res) => {
2025-03-09 22:13:57 -04:00
const payforwhat=req.body.payforwhat;
if (!check_payforwhat(payforwhat,req)) return res.json({ error: 'Invalid PayForWhat' });
try {
// Token or Payment Method ID from the client
const paymentMethodId = req.body.paymentMethodId;
// Create a PaymentIntent on the server
let pennies=0;
if (payforwhat.kind=="new") pennies=Math.round(parseFloat(payforwhat.amounteach))*parseInt(payforwhat.qty) * 100;
else if (payforwhat.kind=="existing") pennies=Math.round(parseFloat(payforwhat.amount))*100;
const return_url=base_url+'/mytickets';
const paymentIntent = await stripe.paymentIntents.create({
amount: pennies, // Amount in cents
currency: 'usd',
payment_method: paymentMethodId,
confirmation_method: 'automatic',
confirm: true, // Attempt to confirm the payment immediately
return_url: return_url,
});
// Check status of payment intent
2025-03-09 22:13:57 -04:00
if (paymentIntent.status === 'requires_action') {
// Additional action is required (e.g. 3D Secure)
2025-03-09 22:13:57 -04:00
res.json({
requiresAction: true,
paymentIntentClientSecret: paymentIntent.client_secret,
});
2025-03-09 22:13:57 -04:00
} else if (paymentIntent.status === 'succeeded') {
// Payment is complete
do_payforwhat(payforwhat);
res.json({ success: true, redirect_url: return_url });
} else {
res.json({ error: 'Invalid PaymentIntent status' });
}
} catch (error) {
console.error('Payment error:', error);
res.json({ error: error.message });
2025-03-09 22:13:57 -04:00
}
2025-02-08 23:22:19 -05:00
});
2024-12-05 13:05:35 -05:00
// Start the server
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
});