2024-12-05 13:05:35 -05:00
const express = require ( 'express' ) ;
const bodyParser = require ( 'body-parser' ) ;
const session = require ( 'express-session' ) ;
2025-02-20 11:12:15 -05:00
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-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
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' ) ) ;
2025-02-20 11:12:15 -05:00
app . use ( cookieParser ( ) ) ;
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-02-25 00:02:58 -05:00
// Convert all the routes to use common.(user,superuser,
// Display messages for all GET routes?
2025-02-20 11:12:15 -05:00
// Setting to deactivate transfers globally
2025-02-25 00:02:58 -05:00
// Option to "Email me my QR Code"
2025-02-20 11:12:15 -05:00
// Mass-import of individual tickets
2025-02-21 20:54:53 -05:00
// + Cookie based QR code functionality
// + Create Account (User)
// + Change Password (User)
// Change most POST routes to end in redirect instead of render.
2024-12-28 19:31:45 -05:00
// Deactivate individual magic links (User)
// Deactivate individual magic links (Admin)
// See how much each camp/ticket has paid (Admin)
2025-02-21 20:54:53 -05:00
// Purchase individual open camping tickets
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
2024-12-05 13:05:35 -05:00
// Make all HTML look nice
2024-12-29 17:20:34 -05:00
// Logging and Replay system(?)
2024-12-28 00:07:12 -05:00
// Stripe Integration
2024-12-29 17:20:34 -05:00
// 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 ) => {
console . log ( "Middleware! req.session=" , req . session ) ;
res . locals . commonData = {
username : req . username , // Attach user info if available
superuser : req . superuser ,
error : req . session && req . session . error || null , // Flash error messages
success : req . session && req . session . success || null , // Flash success messages
} ;
// Clear session-based flash messages after use
if ( req . session ) {
delete req . session . error ;
delete req . session . success ;
}
next ( ) ;
} ) ;
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-02-21 20:54:53 -05:00
let settings = { "enable-transfers" : true } ;
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 : "" } ;
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 } ) ) ;
app . use ( session ( {
secret : 'supersecretkey' ,
resave : false ,
saveUninitialized : false ,
} ) ) ;
// 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 < / h 1 >
< form method = "POST" action = "/emaillink" >
< label > Email Address : < input name = "email" > < / l a b e l >
< button type = "submit" > Submit < / b u t t o n > `
)
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 ) {
2025-02-10 20:53:52 -05:00
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 ;
2025-02-10 20:53:52 -05:00
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
} )
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
2025-02-10 20:53:52 -05:00
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 ;
2025-02-10 20:53:52 -05:00
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-02-21 20:54:53 -05:00
const edit = { username : req . session . username , superuser : req . session . superuser , tickets : { } , cantransfer : settings [ "enable-transfer" ] } ;
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 ;
2025-02-10 20:53:52 -05:00
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
2024-12-26 21:46:13 -05:00
app . get ( '/mytickets' , requireLogin , async ( req , res ) => {
2024-12-26 00:33:23 -05:00
let username = req . session . username ;
let claimed = 0 ;
let owned = 0 ;
let theticket = "" ;
2025-02-21 20:54:53 -05:00
const edit = { username : req . session . username , superuser : req . session . superuser , tickets : { } , cantransfer : settings [ "enable-transfer" ] } ;
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 ;
2025-02-10 20:53:52 -05:00
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-02-10 20:53:52 -05:00
return res . render ( "oneticket" , { username : username , superuser : req . session . superuser , message : message , magiclink : GetMagicLink ( username ) ,
2025-02-21 20:54:53 -05:00
ticket : theticket , offered : tickets [ theticket ] . offered , paid : tickets [ theticket ] . paid , qrcode : dataURL , useurl : useurl ,
cantransfer : settings [ "enable-transfer" ] } ) ;
2024-12-26 21:46:13 -05:00
}
2024-12-26 00:33:23 -05:00
else return res . render ( "manytickets" , edit ) ;
} ) ;
2024-12-19 15:57:33 -05:00
2025-02-10 20:53:52 -05:00
app . get ( "/oneticket" , requireLogin , async ( req , res ) => {
2024-12-26 21:46:13 -05:00
let username = req . session . username ;
2025-02-10 20:53:52 -05:00
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 } ) ;
2025-02-10 20:53:52 -05:00
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 ,
cantransfer : settings [ "enable-transfer" ] } ) ;
2025-02-10 20:53:52 -05:00
} ) ;
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!
2025-02-10 20:53:52 -05:00
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 ;
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" ) {
2025-02-10 20:53:52 -05:00
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' ) ;
2025-02-10 20:53:52 -05:00
const hash1 = hash0 . update ( ticket + QRSalt ) ;
2024-12-26 21:46:13 -05:00
const hash = hash1 . digest ( "base64" ) . slice ( 0 , 6 ) ;
2025-02-10 20:53:52 -05:00
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:45:07 -05:00
// Need a SuperUser version of this
2024-12-28 19:31:45 -05:00
app . post ( "/updateoffered2" , requireLogin , ( req , res ) => {
2025-02-21 20:54:53 -05:00
if ( ! req . cantransfer ) 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 } ) ;
2025-02-10 20:53:52 -05:00
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
2024-12-28 19:31:45 -05: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 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
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 ) => {
2024-12-28 00:07:12 -05:00
if ( req . session . username ) return res . redirect ( "/mytickets" ) ;
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 ( '/' ) ;
} ) ;
} ) ;
2025-02-20 11:12:15 -05:00
app . get ( '/create' , ( req , res ) => {
return res . render ( "create" ) ;
2024-12-05 13:05:35 -05:00
} ) ;
2025-02-20 11:12:15 -05:00
app . post ( '/create' , async ( req , res ) => {
const { username , password1 , password2 } = req . body ;
if ( password1 != password2 ) return res . render ( "error" , { message : "Passwords do not match." } ) ;
if ( users [ username ] && ! users [ username ] . needsconfirm ) return res . render ( "error" , { message : "Email (username) already exists." } ) ;
if ( users [ username ] && users [ username ] . needsconfirm ) {
await client . sendEmail ( { From : "tickets@fallsonfire.net" ,
To : username ,
Subject : "Falls on Fire: Confirm Account Creation" ,
TextBody : "Click here to confirm creation of account " + username ,
HTMLBody : "Click here to confirm creation of account " + username
} ) ;
return res . render ( "message" , { message : "Email has not yet been confirmed. Resent confirm link." } ) ;
}
users [ username ] = { password : hashPW ( password1 ) , needsconfirm : false } ;
console . log ( "Created new account:" , username ) ;
if ( users [ username ] . needsconfirm ) return res . render ( "message" , { message : "Check email to confirm account creation." } ) ;
return res . render ( "message" , { message : "Account created. You may now log in." } ) ;
} ) ;
2024-12-05 13:05:35 -05:00
2025-02-20 11:12:15 -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
2025-02-20 11:12:15 -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
2025-02-20 11:12:15 -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 : "" } ) ;
2025-02-20 11:12:15 -05:00
} ) ;
app . post ( '/changepassword' , requireLogin , ( req , res ) => {
const { password0 , password1 , password2 } = req . body ;
if ( users [ req . session . username ] . password != hashPW ( password0 ) ) return res . render ( "error" , { message : "Old Password is not correct." } ) ;
if ( password1 != password2 ) return res . render ( "error" , { message : "Passwords do not match." } )
users [ req . session . username ] . password = hashPW ( password1 ) ;
return res . render ( "message" , { message : "Password changed." } )
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
} )
2024-12-28 19:31:45 -05:00
2024-12-28 00:07:12 -05:00
app . get ( '/settings' , requireSuperUser , ( req , res ) => {
2025-02-25 00:02:58 -05:00
res . render ( 'settings' , { username : req . session . username , superuser : req . session . superuser , settings : settings , message : "" } )
2024-12-28 00:07:12 -05:00
} ) ;
2024-12-05 13:05:35 -05:00
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-02-21 20:54:53 -05:00
return res . redirect ( "/" ) ; // 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
2024-12-29 17:20:34 -05:00
app . post ( '/update-setting' , requireSuperUser , ( req , res ) => {
settings [ req . body . name ] = req . body . checked ;
2025-02-25 00:02:58 -05:00
console . log ( "setting got updated to " , settings [ req . body . name ] ) ;
2024-12-29 17:20:34 -05:00
res . json ( { success : true , message : 'Checkbox state updated successfully' } ) ;
2025-02-21 20:54:53 -05:00
} ) ;
2024-12-29 17:20:34 -05:00
2025-02-10 20:53:52 -05:00
app . post ( '/pay0' , requireLogin , ( req , res ) => {
return res . render ( "pay" , { username : req . session . username , superuser : req . session . superuser , ticket : req . body . ticket , amount : req . body . amount } ) ;
2025-02-08 23:22:19 -05:00
} ) ;
2025-02-10 20:53:52 -05:00
app . post ( '/charge' , requireLogin , async ( req , res ) => {
const ticket = req . body . ticket ;
if ( tickets [ ticket ] . status == 'r' || tickets [ ticket ] . owner != req . session . username ) {
res . json ( { error : "Sanity check in /charge" } ) ;
} else try {
// Token or Payment Method ID from the client
const paymentMethodId = req . body . paymentMethodId ;
// Create a PaymentIntent on the server
const pennies = Math . round ( parseFloat ( req . body . amount ) * 100 ) ;
console . log ( "Pennies=" , pennies ) ;
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
if ( paymentIntent . status === 'requires_action' ) {
// Additional action is required (e.g. 3D Secure)
res . json ( {
requiresAction : true ,
paymentIntentClientSecret : paymentIntent . client _secret ,
} ) ;
} else if ( paymentIntent . status === 'succeeded' ) {
// Payment is complete
tickets [ ticket ] . paid = pennies ;
console . log ( "Paid " , pennies , " for ticket " , ticket ) ;
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-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 } ` ) ;
} ) ;