Compare commits

...

10 Commits

Author SHA1 Message Date
dc45639450 Fixed .gitignore 2025-03-16 15:20:38 -04:00
395a64c6fb Added .env.example 2025-03-15 13:43:07 -04:00
e79bb5f76d Added .gitignore, package.json, package-lock.json 2025-03-13 23:04:28 -04:00
31cac4e423 changes 2025-03-13 23:01:20 -04:00
3a5db74728 changes 2025-03-13 20:43:27 -04:00
cd1606c9df changes 2025-03-13 20:41:35 -04:00
d074e4b171 changes 2025-03-12 22:40:41 -04:00
f059b231c7 changes 2025-03-12 16:53:29 -04:00
938f2d5564 changes 2025-03-12 19:49:35 +00:00
74808f3fb2 changes 2025-03-11 19:07:16 -04:00
14 changed files with 1974 additions and 127 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
PORT=3000
BASE_URL=http://localhost:3000
STRIPE_SECRET_KEY=sk_test_51QZlMSCHHjDgpHosJ9y6P9rtqJwmaGogCFy1gsZ1HDJyT98LlmSrRFJByawVJbvJh5DEhH6lIcWitsB8vJfPtkus00AomfjTaA

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
.env
*~
temp
public/styles.css

View File

@@ -62,6 +62,16 @@ const QRSalt ="!SaltyMagic5392370662";
// + See how much each camp/ticket has paid (Admin) // + See how much each camp/ticket has paid (Admin)
// + Purchase individual open camping tickets // + Purchase individual open camping tickets
// + Deactivate individual magic links (Admin) // + Deactivate individual magic links (Admin)
// + Get it working again online
// Make all HTML look nice
// 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
// + Make sure subsequent FB imports don't add duplicate tickets
// Maybe: // Maybe:
// Deactivate individual magic links (User) // Deactivate individual magic links (User)
// Option to "Email me my QR Code" // Option to "Email me my QR Code"
@@ -70,7 +80,6 @@ const QRSalt ="!SaltyMagic5392370662";
// + Use a templating engine // + Use a templating engine
// + Store password hashed and salted // + Store password hashed and salted
// + Stripe Integration // + Stripe Integration
// Make all HTML look nice
// Logging and Replay system(?) // Logging and Replay system(?)
// More efficent data structure: TicketsByCamp, TicketsByOffered, TicketsByOwner // More efficent data structure: TicketsByCamp, TicketsByOffered, TicketsByOwner
// //
@@ -175,7 +184,7 @@ app.use((req, res, next) => {
let users={}; let users={};
let tickets={}; let tickets={};
let camps={}; let camps={};
let settings={ "enable-transfer":true }; let settings={ "enable-transfer":true, "open-limit":0 };
function InitDatabase() { function InitDatabase() {
for (const key in users ) delete users[key]; for (const key in users ) delete users[key];
@@ -650,13 +659,29 @@ app.post('/qrcodesu',requireSuperUser,async (req,res) => {
return res.send({ owner:username, qrcode: URL, magiclink:GetMagicLink(username) }); return res.send({ owner:username, qrcode: URL, magiclink:GetMagicLink(username) });
}) })
function opencount() {
let rval=0;
for (t in tickets) if (t.startsWith("open-")) if (tickets[t].status!='r') rval++;
return rval;
}
app.get('/settings',requireSuperUser, (req,res) => { app.get('/settings',requireSuperUser, (req,res) => {
res.render('settings',{ username:req.session.username, superuser:req.session.superuser, settings:settings, message: "" }) 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');
}); });
app.post('/importfb',requireSuperUser,upload.single("file"),(req,res) => { app.post('/importfb',requireSuperUser,upload.single("file"),(req,res) => {
console.log("File name:", req.file.originalname); console.log("File name:", req.file.originalname);
let emails={};
for (t in tickets) {
if (tickets[t].offered) emails[tickets[t].offered]=1;
if (tickets[t].owner ) emails[tickets[t].owner ]=1;
}
const contents=req.file.buffer.toString(); const contents=req.file.buffer.toString();
csvParse.parse(contents, { columns: true, trim: true }, (err, records) => { csvParse.parse(contents, { columns: true, trim: true }, (err, records) => {
if (err) { if (err) {
@@ -665,13 +690,14 @@ app.post('/importfb',requireSuperUser,upload.single("file"),(req,res) => {
return res.redirect("/settings"); return res.redirect("/settings");
} }
let count=0; let count=0;
for (const item of records) { for (const item of records) if (item.email in emails) {
if (!camps[item.camp]) camps[item.camp]={ leader:"", lastid:0 }; if (!camps[item.camp]) camps[item.camp]={ leader:"", lastid:0 };
camps[item.camp].lastid++; camps[item.camp].lastid++;
const ticket=item.camp+"-"+camps[item.camp].lastid; const ticket=item.camp+"-"+camps[item.camp].lastid;
tickets[ticket]={ owner:"", offered: item.email, paid:0.00, status:"i" }; tickets[ticket]={ owner:"", offered: item.email, paid:0.00, status:"i" };
console.log("Offered ",ticket," to ",item.email); console.log("Offered ",ticket," to ",item.email);
count++; count++;
emails[item.email]=1;
} }
req.session.message="Imported "+count+" Frostburn-style records."; req.session.message="Imported "+count+" Frostburn-style records.";
return res.redirect("/settings"); return res.redirect("/settings");
@@ -690,7 +716,7 @@ app.post('/serialize',requireSuperUser, async (req,res) => {
app.post('/deserialize',requireSuperUser, (req,res) => { app.post('/deserialize',requireSuperUser, (req,res) => {
DeserializeAll(); DeserializeAll();
return res.redirect("/"); // Since we may be overwriting session return res.redirect("/settings"); // Since we may be overwriting session
}); });
app.post('/purge',requireSuperUser, (req,res) => { app.post('/purge',requireSuperUser, (req,res) => {
@@ -745,6 +771,7 @@ function do_payforwhat(payforwhat) {
camps[payforwhat.camp].lastid++; camps[payforwhat.camp].lastid++;
tickets[payforwhat.camp+"-"+camps[payforwhat.camp].lastid]={ owner:payforwhat.email, offered:"",paid:100.0*payforwhat.amounteach,status:"i" }; tickets[payforwhat.camp+"-"+camps[payforwhat.camp].lastid]={ owner:payforwhat.email, offered:"",paid:100.0*payforwhat.amounteach,status:"i" };
} }
EmailTickets(payforwhat.email);
} }
} }
@@ -761,6 +788,7 @@ app.post('/buy',requireLogin,(req,res) => {
app.post('/charge', requireLogin, async (req, res) => { app.post('/charge', requireLogin, async (req, res) => {
const payforwhat=req.body.payforwhat; const payforwhat=req.body.payforwhat;
if (!check_payforwhat(payforwhat,req)) return res.json({ error: 'Invalid PayForWhat' }); if (!check_payforwhat(payforwhat,req)) return res.json({ error: 'Invalid PayForWhat' });

1855
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"dependencies": {
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",
"csv-parse": "^5.6.0",
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"express": "^4.21.2",
"express-session": "^1.18.1",
"multer": "^1.4.5-lts.1",
"nodemon": "^3.1.9",
"postmark": "^4.0.5",
"qrcode": "^1.5.4",
"stripe": "^17.6.0"
}
}

View File

@@ -35,24 +35,37 @@
cursor: pointer; cursor: pointer;
} }
h1 {
font-size: 1.2rem;
}
body { body {
display: grid; display: grid;
grid-template-columns: 240px auto; grid-template-columns: 240px auto;
grid-template-rows: 40px auto; grid-template-rows: 40px auto;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
} }
.nav { .nav-header {
font-size: 1.5rem;
padding: 10px 15px;
grid-column: 1 / 3;
grid-row: 1;
background-color: #C0C0C0;
}
.nav-items {
padding: 10px 15px;
grid-column: 1; grid-column: 1;
grid-row: 1 / 3; grid-row: 2 / 3;
background-color: #FFC0C0; background-color: #C0C0C0;
} }
.message { .message {
grid-column: 2; grid-column: 2;
grid-row: 1; grid-row: 1;
background-color: #C0FFC0; background-color: #C0C0C0;
align-content: center; align-content: center;
justify-items: center; justify-items: center;
} }
@@ -60,5 +73,5 @@ body {
.content { .content {
grid-column: 2; grid-column: 2;
grid-row: 2; grid-row: 2;
background-color: #C0C0FF; background-color: #E0E0E0;
} }

View File

@@ -7,7 +7,7 @@
<body> <body>
<%- include('partials/nav') %> <%- include('partials/nav') %>
<div class="content"> <div class="content">
Change Password <h1>Change Password</h1>
<form id="editor" method="POST" action="/changepassword"> <form id="editor" method="POST" action="/changepassword">
Old Password:<input type="password" name="password0"><br> Old Password:<input type="password" name="password0"><br>
New Password:<input type="password" name="password1"><br> New Password:<input type="password" name="password1"><br>

View File

@@ -1,9 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>You don't have any tickets</title>
</head>
<body>
<h1>No tickets for you!!</h1>
</body>
</html>

View File

@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Your Tickets</title>
</head>
<body>
<h1>Welcome, <%= username %>!</h1>
<table border="1">
<tr>
<th>Ticket</th>
<th>Owner</th>
<th>Offered To</th>
<th>QRCode</th>
</tr>
<% for (const t in tlist) { %>
<tr>
<td> <%= t %> </td>
<td> <%= tlist[t].owner %> </td>
<td> <%= tlist[t].offered %> </td>
<td> <%= QRCode.toDataURL("https://abc.com", function(err,url) { if (err) { console.error(err); return; } console.log(url); }); %> </td>
</tr>
<% } %>
<tr>
</tr>
</table>
</body>
</html>

View File

@@ -1,26 +1,28 @@
<!-- views/partials/nav.ejs --> <!-- views/partials/nav.ejs -->
<div class="nav"> <div class="nav-header">
Falls on Fire<br> Falls on Fire
</div>
<div class="nav-items">
<a href="/buy">Buy Tickets</a><br> <a href="/buy">Buy Tickets</a><br>
<%if (typeof username!='undefined' && username) {%> <%if (typeof username!='undefined' && username) {%>
<a href="/mytickets">View My Tickets</a><br> <a href="/mytickets">View My Tickets</a><br>
<%}%> <%}%>
<%if (typeof superuser!='undefined' && superuser) {%> <%if (typeof superuser!='undefined' && superuser) {%>
<a href="/camps">View Camps (Admin)</a><br> <a href="/camps">View Camps (Admin)</a><br>
<a href="/settings">Settings (Admin)</a><br> <a href="/settings">Settings (Admin)</a><br>
<%}%> <%}%>
<%if (typeof username!='undefined' && username) {%> <%if (typeof username!='undefined' && username) {%>
<a href="/changepassword">Change Password</a><br> <a href="/changepassword">Change Password</a><br>
<a href="/logout">Log Out</a><br> <a href="/logout">Log Out</a><br>
<%} else {%> <%} else {%>
<a href="/create">Create Account</a><br> <a href="/create">Create Account</a><br>
<a href="/login">Log In</a><br> <a href="/login">Log In</a><br>
<%}%> <%}%>
</div> </div>
<div class="message" id="message"> <div class="message" id="message">
<% if (typeof commonData.message !== 'undefined') { %> <% if (typeof commonData.message !== 'undefined') { %>
<p><%= commonData.message %></p> <p><%= commonData.message %></p>
<% } %> <% } %>
</div> </div>
<% if (commonData.error) { %> <% if (commonData.error) { %>
<div id="errorModal"> <div id="errorModal">

12
views/partials/nav.ejs~ Normal file
View File

@@ -0,0 +1,12 @@
<!-- views/partials/nav.ejs -->
<aside>
<nav class="nav-links">
Falls on Fire
<ul>
<li><a href="/mytickets">View My Tickets</a></li>
<li><a href="/camps">View Camps (Admin)</a></li>
<li><a href="/settings">Settings (Admin)</a></li>
<li><a href="/logout">Log Out</a></li>
</ul>
</nav>
</aside>

View File

@@ -28,6 +28,11 @@
<input type="email" name="email"> <input type="email" name="email">
<button type="submit">Deactivate</button> <button type="submit">Deactivate</button>
</form> </form>
<form action="/addopen" method="post">
Open Camping Tickets: <%=opencount%> / <%=settings['open-limit']%>
<input type="number" name="addopen">
<button type="submit">Add</button>
</form
<form action="/importfb" method="post" enctype="multipart/form-data"> <form action="/importfb" method="post" enctype="multipart/form-data">
Import Tickets (Frostburn Format): Import Tickets (Frostburn Format):
<input type="file" name="file"> <input type="file" name="file">

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Your Tickets</title>
</head>
<body>
<h1>Welcome, <%= username %>!</h1>
<table border="1">
<tr>
<th>Ticket</th>
<th>Owner</th>
<th>Offered To</th>
<th>QRCode</th>
</tr>
<% for (const t in tlist) { %>
<tr>
<td> <%= t %> </td>
<td> <%= tlist[t].owner %> </td>
<td> <%= tlist[t].offered %> </td>
<td> <%= simpledata %> </td>
<td> <%= QRCode.toDataURL("https://abc.com", function(err,url) { if (err) { console.error(err); return; } console.log(url); }); %> </td>
</tr>
<% } %>
<tr>
</tr>
</table>
</body>
</html>

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Your Tickets</title>
</head>
<body>
<h1>Welcome, <%= username %>!</h1>
<table border="1">
<tr>
<th>Ticket</th>
<th>Owner</th>
<th>Offered To</th>
<th>QRCode</th>
</tr>
<% for (const t in tlist) { %>
<tr>
<td> <%= t %> </td>
<td> <%= tlist[t].owner %> </td>
<td> <%= tlist[t].offered %> </td>
<td> <%= simpledata %> </td>
<td> <%= QRCode.toDataURL("https://abc.com", function(err,url) { if (err) { console.error(err); return; } console.log(url); }); %> </td>
</tr>
<% } %>
<tr>
</tr>
</table>
</body>
</html>