diff --git a/client/geoword/confirmation.html b/client/geoword/confirmation.html
new file mode 100644
index 000000000..bd40c9767
--- /dev/null
+++ b/client/geoword/confirmation.html
@@ -0,0 +1,77 @@
+
+
+
+
+ QB Reader
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/geoword/index.js b/client/geoword/index.js
index 684f8a5e6..6dc0d29b2 100644
--- a/client/geoword/index.js
+++ b/client/geoword/index.js
@@ -11,7 +11,7 @@ fetch('/api/geoword/packet-list')
const gameListSelect = document.getElementById('packet-list');
packetList.forEach(game => {
const a = document.createElement('a');
- a.href = '/geoword/division/' + game.name;
+ a.href = '/geoword/' + (game.costInCents ? 'payment/' : 'division/') + game.name;
a.textContent = titleCase(game.name);
const li = document.createElement('li');
diff --git a/client/geoword/payment.html b/client/geoword/payment.html
new file mode 100644
index 000000000..e11b31c01
--- /dev/null
+++ b/client/geoword/payment.html
@@ -0,0 +1,93 @@
+
+
+
+
+ QB Reader
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The cost to play the packet is $2.00.
+ Please enter payment information below:
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/geoword/payment.js b/client/geoword/payment.js
new file mode 100644
index 000000000..4fb00131c
--- /dev/null
+++ b/client/geoword/payment.js
@@ -0,0 +1,127 @@
+const packetName = window.location.pathname.split('/').pop();
+document.getElementById('packet-name').textContent = titleCase(packetName);
+
+// This is your test publishable API key.
+const STRIPE_PUBLISHABLE_KEY = 'pk_live_51NManVKG9mAb0mOpZxtFcYWRju7COWAwtirGyd01es3bEJhqSZd8SdSsOPgyj2LizN0QYjLumsWiOoB2nKadXrt100bTtyHh8m';
+const stripe = Stripe(STRIPE_PUBLISHABLE_KEY);
+
+let elements;
+
+initialize();
+checkStatus();
+
+document.querySelector('#payment-form').addEventListener('submit', handleSubmit);
+
+let emailAddress = '';
+// Fetches a payment intent and captures the client secret
+async function initialize() {
+ const response = await fetch('/api/geoword/create-payment-intent', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ packetName: packetName }),
+ });
+ const { clientSecret } = await response.json();
+
+ const isDarkTheme = (localStorage.getItem('color-theme') === 'dark') || (!localStorage.getItem('color-theme') && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
+ const appearance = {
+ theme: isDarkTheme ? 'night' : 'stripe'
+ };
+ elements = stripe.elements({ appearance, clientSecret });
+
+ const linkAuthenticationElement = elements.create('linkAuthentication');
+ linkAuthenticationElement.mount('#link-authentication-element');
+
+ linkAuthenticationElement.on('change', (event) => {
+ emailAddress = event.value.email;
+ });
+
+ const paymentElementOptions = {
+ layout: 'tabs',
+ };
+
+ const paymentElement = elements.create('payment', paymentElementOptions);
+ paymentElement.mount('#payment-element');
+}
+
+async function handleSubmit(e) {
+ e.preventDefault();
+ setLoading(true);
+
+ const { error } = await stripe.confirmPayment({
+ elements,
+ confirmParams: {
+ // Make sure to change this to your payment completion page
+ return_url: window.location.origin + '/geoword/confirmation',
+ receipt_email: emailAddress,
+ },
+ });
+
+ // This point will only be reached if there is an immediate error when
+ // confirming the payment. Otherwise, your customer will be redirected to
+ // your `return_url`. For some payment methods like iDEAL, your customer will
+ // be redirected to an intermediate site first to authorize the payment, then
+ // redirected to the `return_url`.
+ if (error.type === 'card_error' || error.type === 'validation_error') {
+ showMessage(error.message);
+ } else {
+ showMessage('An unexpected error occurred.');
+ }
+
+ setLoading(false);
+}
+
+// Fetches the payment intent status after payment submission
+async function checkStatus() {
+ const clientSecret = new URLSearchParams(window.location.search).get(
+ 'payment_intent_client_secret'
+ );
+
+ if (!clientSecret) {
+ return;
+ }
+
+ const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
+
+ switch (paymentIntent.status) {
+ case 'succeeded':
+ showMessage('Payment succeeded!');
+ break;
+ case 'processing':
+ showMessage('Your payment is processing.');
+ break;
+ case 'requires_payment_method':
+ showMessage('Your payment was not successful, please try again.');
+ break;
+ default:
+ showMessage('Something went wrong.');
+ break;
+ }
+}
+
+// ------- UI helpers -------
+
+function showMessage(messageText) {
+ const messageContainer = document.querySelector('#payment-message');
+
+ messageContainer.classList.remove('hidden');
+ messageContainer.textContent = messageText;
+
+ setTimeout(function () {
+ messageContainer.classList.add('hidden');
+ messageContainer.textContent = '';
+ }, 4000);
+}
+
+// Show a spinner on payment submission
+function setLoading(isLoading) {
+ if (isLoading) {
+ // Disable the button and show a spinner
+ document.querySelector('#submit').disabled = true;
+ document.querySelector('#spinner').classList.remove('hidden');
+ document.querySelector('#button-text').classList.add('hidden');
+ } else {
+ document.querySelector('#submit').disabled = false;
+ document.querySelector('#spinner').classList.add('hidden');
+ document.querySelector('#button-text').classList.remove('hidden');
+ }
+}
diff --git a/database/geoword.js b/database/geoword.js
index 00946fc2e..574124b4f 100644
--- a/database/geoword.js
+++ b/database/geoword.js
@@ -12,9 +12,28 @@ const geoword = client.db('geoword');
const buzzes = geoword.collection('buzzes');
const divisionChoices = geoword.collection('division-choices');
const packets = geoword.collection('packets');
+const payments = geoword.collection('payments');
const tossups = geoword.collection('tossups');
+/**
+ * Returns true if the user has paid for the packet,
+ * or if the packet is free.
+ * @param {*} param0
+ * @returns {Promise}
+ */
+async function checkPayment({ packetName, username }) {
+ const packet = await packets.findOne({ name: packetName });
+ if (!packet) {
+ return false;
+ } else if (packet.costInCents === 0) {
+ return true;
+ }
+
+ const user_id = await getUserId(username);
+ const result = await payments.findOne({ packetName, user_id });
+ return !!result;
+}
async function getAdminStats(packetName, division) {
const stats = await buzzes.aggregate([
@@ -79,12 +98,22 @@ async function getBuzzCount(packetName, username) {
return await buzzes.countDocuments({ packetName, user_id });
}
+/**
+ *
+ * @param {*} packetName
+ * @returns {Integer} the cost to play the packet, in cents (USD)
+ */
+async function getCost(packetName) {
+ const packet = await packets.findOne({ name: packetName });
+ return packet?.costInCents;
+}
+
async function getDivisionChoice(packetName, username) {
const user_id = await getUserId(username);
- return await getDivisionById(packetName, user_id);
+ return await getDivisionChoiceById(packetName, user_id);
}
-async function getDivisionById(packetName, user_id) {
+async function getDivisionChoiceById(packetName, user_id) {
const result = await divisionChoices.findOne({ packetName, user_id });
return result?.division;
}
@@ -144,7 +173,7 @@ async function getProgress(packetName, username) {
]).toArray();
result[0] = result[0] || {};
- result[0].division = await getDivisionById(packetName, user_id);
+ result[0].division = await getDivisionChoiceById(packetName, user_id);
return result[0];
}
@@ -177,7 +206,7 @@ async function getQuestionCount(packetName) {
* @param {ObjectId} user_id
*/
async function getUserStats({ packetName, user_id }) {
- const division = await getDivisionById(packetName, user_id);
+ const division = await getDivisionChoiceById(packetName, user_id);
const buzzArray = await buzzes.aggregate([
{ $match: { packetName, user_id } },
@@ -250,7 +279,7 @@ async function getUserStats({ packetName, user_id }) {
* @param {ObjectId} params.user_id
*/
async function recordBuzz({ celerity, givenAnswer, points, packetName, questionNumber, user_id }) {
- const division = await getDivisionById(packetName, user_id);
+ const division = await getDivisionChoiceById(packetName, user_id);
await buzzes.replaceOne(
{ user_id, packetName, questionNumber },
@@ -270,6 +299,14 @@ async function recordDivision({ packetName, username, division }) {
);
}
+async function recordPayment({ packetName, user_id }) {
+ return payments.replaceOne(
+ { user_id, packetName },
+ { user_id, packetName, createdAt: new Date() },
+ { upsert: true },
+ );
+}
+
async function recordProtest({ packetName, questionNumber, username }) {
const user_id = await getUserId(username);
return await buzzes.updateOne(
@@ -295,9 +332,11 @@ async function resolveProtest({ id, decision, reason }) {
}
export {
+ checkPayment,
getAdminStats,
getAnswer,
getBuzzCount,
+ getCost,
getDivisionChoice,
getDivisions,
getLeaderboard,
@@ -308,6 +347,7 @@ export {
getUserStats,
recordBuzz,
recordDivision,
+ recordPayment,
recordProtest,
resolveProtest,
};
diff --git a/database/questions.js b/database/questions.js
index 987e0ce0d..66d0f7d58 100644
--- a/database/questions.js
+++ b/database/questions.js
@@ -1,5 +1,3 @@
-import 'dotenv/config';
-
import { OKCYAN, ENDC, OKGREEN } from '../bcolors.js';
import { ADJECTIVES, ANIMALS, DEFAULT_QUERY_RETURN_LENGTH, MAX_QUERY_RETURN_LENGTH, DIFFICULTIES, CATEGORIES, SUBCATEGORIES_FLATTENED, DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR } from '../constants.js';
diff --git a/package-lock.json b/package-lock.json
index b4796269f..585869f4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"bootstrap": "5.2.3",
"cookie-session": "^2.0.0",
+ "cors": "^2.8.5",
"damerau-levenshtein-js": "^1.1.8",
"dompurify": "^3.0.0",
"dotenv": "^16.0.1",
@@ -25,6 +26,7 @@
"react-dom": "^18.2.0",
"roman-numerals": "^0.3.2",
"stemmer": "^2.0.1",
+ "stripe": "^12.10.0",
"uuid": "^8.3.2",
"ws": "^8.8.0"
},
@@ -1476,6 +1478,18 @@
"node": ">= 0.8"
}
},
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3346,7 +3360,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4206,6 +4219,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/stripe": {
+ "version": "12.10.0",
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-12.10.0.tgz",
+ "integrity": "sha512-JJS4Bzx4LAzS8cTy26wqCdRQmhUtyV2XjFXfFTXa+W+Won2PXEWu85RpROi1ZN6pNFcWH7xrhXVW0OfCxUmwwA==",
+ "dependencies": {
+ "@types/node": ">=8.1.0",
+ "qs": "^6.11.0"
+ },
+ "engines": {
+ "node": ">=12.*"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -5640,6 +5665,15 @@
"keygrip": "~1.1.0"
}
},
+ "cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "requires": {
+ "object-assign": "^4",
+ "vary": "^1"
+ }
+ },
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -7022,8 +7056,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
"object-inspect": {
"version": "1.12.2",
@@ -7623,6 +7656,15 @@
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true
},
+ "stripe": {
+ "version": "12.10.0",
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-12.10.0.tgz",
+ "integrity": "sha512-JJS4Bzx4LAzS8cTy26wqCdRQmhUtyV2XjFXfFTXa+W+Won2PXEWu85RpROi1ZN6pNFcWH7xrhXVW0OfCxUmwwA==",
+ "requires": {
+ "@types/node": ">=8.1.0",
+ "qs": "^6.11.0"
+ }
+ },
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
diff --git a/package.json b/package.json
index 07f7054d3..c205c8b26 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"dependencies": {
"bootstrap": "5.2.3",
"cookie-session": "^2.0.0",
+ "cors": "^2.8.5",
"damerau-levenshtein-js": "^1.1.8",
"dompurify": "^3.0.0",
"dotenv": "^16.0.1",
@@ -29,6 +30,7 @@
"react-dom": "^18.2.0",
"roman-numerals": "^0.3.2",
"stemmer": "^2.0.1",
+ "stripe": "^12.10.0",
"uuid": "^8.3.2",
"ws": "^8.8.0"
},
diff --git a/routes/api/geoword.js b/routes/api/geoword.js
index c51208ad5..7ec855bc5 100644
--- a/routes/api/geoword.js
+++ b/routes/api/geoword.js
@@ -4,9 +4,11 @@ import { checkToken } from '../../server/authentication.js';
import checkAnswer from '../../server/checkAnswer.js';
import { Router } from 'express';
+import stripeClass from 'stripe';
import { ObjectId } from 'mongodb';
const router = Router();
+const stripe = new stripeClass(process.env.STRIPE_SECRET_KEY);
router.get('/admin/protests', async (req, res) => {
const { username, token } = req.session;
@@ -74,6 +76,32 @@ router.get('/admin/stats', async (req, res) => {
res.json({ stats });
});
+router.post('/create-payment-intent', async (req, res) => {
+ const { username, token } = req.session;
+ if (!checkToken(username, token)) {
+ delete req.session;
+ res.sendStatus(401);
+ return;
+ }
+
+ const user_id = await getUserId(username);
+ const packetName = req.body.packetName;
+
+ // Create a PaymentIntent with the order amount and currency
+ const paymentIntent = await stripe.paymentIntents.create({
+ amount: 200, // $2.00
+ currency: 'usd',
+ automatic_payment_methods: {
+ enabled: true,
+ },
+ metadata: { user_id: String(user_id), packetName: packetName },
+ });
+
+ res.send({
+ clientSecret: paymentIntent.client_secret,
+ });
+});
+
router.get('/check-answer', async (req, res) => {
const { givenAnswer, questionNumber, packetName } = req.query;
const answer = await geoword.getAnswer(packetName, parseInt(questionNumber));
@@ -117,6 +145,29 @@ router.get('/get-question-count', async (req, res) => {
res.json({ questionCount });
});
+router.get('/record-buzz', async (req, res) => {
+ const { username, token } = req.session;
+ if (!checkToken(username, token)) {
+ delete req.session;
+ res.sendStatus(401);
+ return;
+ }
+
+ req.query.celerity = parseFloat(req.query.celerity);
+ req.query.points = parseInt(req.query.points);
+ req.query.questionNumber = parseInt(req.query.questionNumber);
+
+ const user_id = await getUserId(username);
+ const { packetName, questionNumber, celerity, points, givenAnswer } = req.query;
+ const result = await geoword.recordBuzz({ celerity, points, packetName, questionNumber, givenAnswer, user_id });
+
+ if (result) {
+ res.sendStatus(200);
+ } else {
+ res.sendStatus(500);
+ }
+});
+
router.put('/record-division', async (req, res) => {
const { username, token } = req.session;
if (!checkToken(username, token)) {
@@ -168,27 +219,4 @@ router.get('/stats', async (req, res) => {
res.json({ buzzArray, division, leaderboard });
});
-router.get('/record-buzz', async (req, res) => {
- const { username, token } = req.session;
- if (!checkToken(username, token)) {
- delete req.session;
- res.sendStatus(401);
- return;
- }
-
- req.query.celerity = parseFloat(req.query.celerity);
- req.query.points = parseInt(req.query.points);
- req.query.questionNumber = parseInt(req.query.questionNumber);
-
- const user_id = await getUserId(username);
- const { packetName, questionNumber, celerity, points, givenAnswer } = req.query;
- const result = await geoword.recordBuzz({ celerity, points, packetName, questionNumber, givenAnswer, user_id });
-
- if (result) {
- res.sendStatus(200);
- } else {
- res.sendStatus(500);
- }
-});
-
export default router;
diff --git a/routes/api/webhook.js b/routes/api/webhook.js
new file mode 100644
index 000000000..cd990a255
--- /dev/null
+++ b/routes/api/webhook.js
@@ -0,0 +1,54 @@
+import * as geoword from '../../database/geoword.js';
+
+import { Router } from 'express';
+import { ObjectId } from 'mongodb';
+import stripeClass from 'stripe';
+
+const router = Router();
+const stripe = new stripeClass(process.env.STRIPE_SECRET_KEY);
+
+// This is your test secret API key.
+// Replace this endpoint secret with your endpoint's unique secret
+// If you are testing with the CLI, find the secret by running 'stripe listen'
+// If you are using an endpoint defined with the API or dashboard, look in your webhook settings
+// at https://dashboard.stripe.com/webhooks
+const endpointSecret = process.env.STRIPE_SIGNING_SECRET;
+
+router.post('/', (req, res) => {
+ const sig = req.headers['stripe-signature'];
+
+ let event;
+
+ try {
+ event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
+ } catch (err) {
+ res.status(400).send(`Webhook Error: ${err.message}`);
+ return;
+ }
+
+ // Handle the event
+ switch (event.type) {
+ case 'payment_intent.succeeded': {
+ const paymentIntentSucceeded = event.data.object;
+ // Then define and call a function to handle the event payment_intent.succeeded
+ const { user_id, packetName } = paymentIntentSucceeded.metadata;
+ geoword.recordPayment({ packetName, user_id: new ObjectId(user_id) });
+ break;
+ }
+
+ case 'charge.succeeded':
+ // We can safely ignore this
+ res.send();
+ return;
+
+ default:
+ // ... handle other event types
+ // console.log(`Unhandled event type ${event.type}`);
+ }
+
+ // Return a 200 response to acknowledge receipt of the event
+ console.log('Received event:', event.type);
+ res.send();
+});
+
+export default router;
diff --git a/routes/geoword/admin.js b/routes/geoword/admin.js
new file mode 100644
index 000000000..870ae3962
--- /dev/null
+++ b/routes/geoword/admin.js
@@ -0,0 +1,39 @@
+import { isAdmin } from '../../database/users.js';
+import { checkToken } from '../../server/authentication.js';
+
+import { Router } from 'express';
+
+const router = Router();
+
+router.use(async (req, res, next) => {
+ const { username, token } = req.session;
+
+ if (!checkToken(username, token)) {
+ delete req.session;
+ res.redirect('/geoword/login');
+ return;
+ }
+
+ const admin = await isAdmin(username);
+
+ if (!admin) {
+ res.redirect('/geoword');
+ return;
+ }
+
+ next();
+});
+
+router.get('/', async (req, res) => {
+ res.sendFile('index.html', { root: './client/geoword/admin' });
+});
+
+router.get('/protests/:packetName/:division', async (req, res) => {
+ res.sendFile('protests.html', { root: './client/geoword/admin' });
+});
+
+router.get('/stats/:packetName/:division', async (req, res) => {
+ res.sendFile('stats.html', { root: './client/geoword/admin' });
+});
+
+export default router;
diff --git a/routes/geoword.js b/routes/geoword/index.js
similarity index 66%
rename from routes/geoword.js
rename to routes/geoword/index.js
index 3397b1253..d5cbfe8eb 100644
--- a/routes/geoword.js
+++ b/routes/geoword/index.js
@@ -1,6 +1,6 @@
-import * as geoword from '../database/geoword.js';
-import { isAdmin } from '../database/users.js';
-import { checkToken } from '../server/authentication.js';
+import * as geoword from '../../database/geoword.js';
+import { checkToken } from '../../server/authentication.js';
+import adminRouter from './admin.js';
import { Router } from 'express';
@@ -10,59 +10,7 @@ router.get('/', (req, res) => {
res.sendFile('index.html', { root: './client/geoword' });
});
-router.get('/admin', async (req, res) => {
- const { username, token } = req.session;
- if (!checkToken(username, token)) {
- delete req.session;
- res.redirect('/geoword/login');
- return;
- }
-
- const admin = await isAdmin(username);
-
- if (!admin) {
- res.redirect('/geoword');
- return;
- }
-
- res.sendFile('index.html', { root: './client/geoword/admin' });
-});
-
-router.get('/admin/protests/:packetName/:division', async (req, res) => {
- const { username, token } = req.session;
- if (!checkToken(username, token)) {
- delete req.session;
- res.redirect('/geoword/login');
- return;
- }
-
- const admin = await isAdmin(username);
-
- if (!admin) {
- res.redirect('/geoword');
- return;
- }
-
- res.sendFile('protests.html', { root: './client/geoword/admin' });
-});
-
-router.get('/admin/stats/:packetName/:division', async (req, res) => {
- const { username, token } = req.session;
- if (!checkToken(username, token)) {
- delete req.session;
- res.redirect('/geoword/login');
- return;
- }
-
- const admin = await isAdmin(username);
-
- if (!admin) {
- res.redirect('/geoword');
- return;
- }
-
- res.sendFile('stats.html', { root: './client/geoword/admin' });
-});
+router.use('/admin', adminRouter);
router.get('/audio/*.mp3', (req, res) => {
const url = req.url.substring(7);
@@ -74,6 +22,10 @@ router.get('/audio', (req, res) => {
res.sendFile(`${packetName}/${division}/${currentQuestionNumber}.mp3`, { root: './geoword-audio' });
});
+router.get('/confirmation', (req, res) => {
+ res.sendFile('confirmation.html', { root: './client/geoword' });
+});
+
router.get('/division/:packetName', async (req, res) => {
const { username, token } = req.session;
if (!checkToken(username, token)) {
@@ -82,9 +34,18 @@ router.get('/division/:packetName', async (req, res) => {
return;
}
- const divisionChoice = await geoword.getDivisionChoice(req.params.packetName, username);
+ const packetName = req.params.packetName;
+ const divisionChoice = await geoword.getDivisionChoice(packetName, username);
+
if (divisionChoice) {
- res.redirect('/geoword/game/' + req.params.packetName);
+ res.redirect('/geoword/game/' + packetName);
+ return;
+ }
+
+ const paid = await geoword.checkPayment({ packetName, username });
+
+ if (!paid) {
+ res.redirect('/geoword/payment/' + packetName);
return;
}
@@ -100,13 +61,20 @@ router.get('/game/:packetName', async (req, res) => {
}
const packetName = req.params.packetName;
-
const divisionChoice = await geoword.getDivisionChoice(packetName, username);
+
if (!divisionChoice) {
res.redirect('/geoword/division/' + packetName);
return;
}
+ const paid = await geoword.checkPayment({ packetName, username });
+
+ if (!paid) {
+ res.redirect('/geoword/payment/' + packetName);
+ return;
+ }
+
const [buzzCount, questionCount] = await Promise.all([
geoword.getBuzzCount(packetName, username),
geoword.getQuestionCount(packetName),
@@ -120,6 +88,10 @@ router.get('/game/:packetName', async (req, res) => {
res.sendFile('game.html', { root: './client/geoword' });
});
+router.get('/index', (req, res) => {
+ res.redirect('/geoword');
+});
+
router.get('/leaderboard/:packetName/:division', (req, res) => {
const { username, token } = req.session;
if (!checkToken(username, token)) {
@@ -131,15 +103,11 @@ router.get('/leaderboard/:packetName/:division', (req, res) => {
res.sendFile('leaderboard.html', { root: './client/geoword' });
});
-router.get('/index', (req, res) => {
- res.redirect('/geoword');
-});
-
router.get('/login', (req, res) => {
res.sendFile('login.html', { root: './client/geoword' });
});
-router.get('/stats/:packetName', (req, res) => {
+router.get('/payment/:packetName', async (req, res) => {
const { username, token } = req.session;
if (!checkToken(username, token)) {
delete req.session;
@@ -147,6 +115,33 @@ router.get('/stats/:packetName', (req, res) => {
return;
}
+ const packetName = req.params.packetName;
+ const paid = await geoword.checkPayment({ packetName, username });
+
+ if (paid) {
+ res.redirect('/geoword/division/' + packetName);
+ return;
+ }
+
+ res.sendFile('payment.html', { root: './client/geoword' });
+});
+
+router.get('/stats/:packetName', async (req, res) => {
+ const { username, token } = req.session;
+ if (!checkToken(username, token)) {
+ delete req.session;
+ res.redirect('/geoword/login');
+ return;
+ }
+
+ const packetName = req.params.packetName;
+ const paid = await geoword.checkPayment({ packetName, username });
+
+ if (!paid) {
+ res.redirect('/geoword/payment/' + packetName);
+ return;
+ }
+
res.sendFile('stats.html', { root: './client/geoword' });
});
diff --git a/server/authentication.js b/server/authentication.js
index 5fceb9643..939c541e1 100644
--- a/server/authentication.js
+++ b/server/authentication.js
@@ -1,5 +1,3 @@
-import 'dotenv/config';
-
import { getUserField, getUserId, updateUser, verifyEmail } from '../database/users.js';
import { createHash } from 'crypto';
diff --git a/server/server.js b/server/server.js
index 0da97549f..afa4896b1 100644
--- a/server/server.js
+++ b/server/server.js
@@ -2,6 +2,7 @@ import 'dotenv/config';
import { ipFilterMiddleware, ipFilterError } from './ip-filter.js';
import { createAndReturnRoom } from './TossupRoom.js';
+
import { WEBSOCKET_MAX_PAYLOAD, COOKIE_MAX_AGE } from '../constants.js';
import aboutRouter from '../routes/about.js';
import apiRouter from '../routes/api/index.js';
@@ -10,14 +11,15 @@ import authRouter from '../routes/auth.js';
import backupsRouter from '../routes/backups.js';
import bonusesRouter from '../routes/bonuses.js';
import databaseRouter from '../routes/database.js';
-import geowordRouter from '../routes/geoword.js';
+import geowordRouter from '../routes/geoword/index.js';
+import indexRouter from '../routes/index.js';
import multiplayerRouter from '../routes/multiplayer.js';
import tossupsRouter from '../routes/tossups.js';
import userRouter from '../routes/user.js';
-import indexRouter from '../routes/index.js';
+import webhookRouter from '../routes/api/webhook.js';
import cookieSession from 'cookie-session';
-import express, { json } from 'express';
+import express from 'express';
import { createServer } from 'http';
import { v4 } from 'uuid';
import { WebSocketServer } from 'ws';
@@ -34,7 +36,8 @@ const wss = new WebSocketServer({
// for why we use 'simple'
app.set('query parser', 'simple');
-app.use(json());
+app.use('/api/webhook', express.raw({ type: '*/*' }), webhookRouter);
+app.use(express.json());
app.use(cookieSession({
name: 'session',
@@ -117,6 +120,7 @@ app.use('/geoword', geowordRouter);
app.use('/multiplayer', multiplayerRouter);
app.use('/tossups', tossupsRouter);
app.use('/user', userRouter);
+app.use('/webhook', webhookRouter);
app.use('/', indexRouter);
/**