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 + + + + + + + + + + + + + + + + +
+

+ Payment confirmed. + Return to geoword. +

+
+ + + + + + + + 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); /**