diff --git a/client/geoword/division.js b/client/geoword/division.js index 99163aa80..e7ba1dab5 100644 --- a/client/geoword/division.js +++ b/client/geoword/division.js @@ -3,7 +3,7 @@ const packetTitle = titleCase(packetName); document.getElementById('packet-name').textContent = packetTitle; -fetch('/geoword/api/get-divisions?' + new URLSearchParams({ packetName })) +fetch('/api/geoword/get-divisions?' + new URLSearchParams({ packetName })) .then(response => response.json()) .then(data => { const { divisions } = data; @@ -18,7 +18,7 @@ fetch('/geoword/api/get-divisions?' + new URLSearchParams({ packetName })) document.getElementById('form').addEventListener('submit', async event => { const division = document.getElementById('division').value; - fetch('/geoword/api/record-division', { + fetch('/api/geoword/record-division', { method: 'PUT', headers: { 'Content-Type': 'application/json', diff --git a/client/geoword/game.js b/client/geoword/game.js index 2da62c17f..0596177aa 100644 --- a/client/geoword/game.js +++ b/client/geoword/game.js @@ -16,14 +16,14 @@ let tossupsHeard = 0; document.getElementById('geoword-stats').href = '/geoword/stats/' + packetName; -fetch('/geoword/api/get-question-count?' + new URLSearchParams({ packetName })) +fetch('/api/geoword/get-question-count?' + new URLSearchParams({ packetName })) .then(response => response.json()) .then(data => { packetLength = data.questionCount; document.getElementById('packet-length').textContent = packetLength; }); -fetch('/geoword/api/get-progress?' + new URLSearchParams({ packetName })) +fetch('/api/geoword/get-progress?' + new URLSearchParams({ packetName })) .then(response => response.json()) .then(data => { ({ division, numberCorrect, points, totalCorrectCelerity, tossupsHeard } = data); @@ -48,7 +48,7 @@ const incorrectAudio = new Audio('/geoword/audio/incorrect.mp3'); const sampleAudio = new Audio('/geoword/audio/sample.mp3'); async function checkGeowordAnswer(givenAnswer, questionNumber) { - return await fetch('/geoword/api/check-answer?' + new URLSearchParams({ + return await fetch('/api/geoword/check-answer?' + new URLSearchParams({ givenAnswer, packetName, questionNumber, @@ -106,7 +106,7 @@ function next() { } function recordProtest(packetName, questionNumber) { - fetch('/geoword/api/record-protest?', { + fetch('/api/geoword/record-protest?', { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -123,7 +123,7 @@ function recordProtest(packetName, questionNumber) { } function recordBuzz(packetName, questionNumber, celerity, points, givenAnswer) { - fetch('/geoword/api/record-buzz?' + new URLSearchParams({ + fetch('/api/geoword/record-buzz?' + new URLSearchParams({ packetName, questionNumber, celerity, diff --git a/client/geoword/index.js b/client/geoword/index.js index fa5f7ccd6..ed35a0608 100644 --- a/client/geoword/index.js +++ b/client/geoword/index.js @@ -4,7 +4,7 @@ getAccountUsername().then(username => { } }); -fetch('/geoword/api/packet-list') +fetch('/api/geoword/packet-list') .then(response => response.json()) .then(data => { const { packetList } = data; diff --git a/client/geoword/leaderboard.js b/client/geoword/leaderboard.js index db9a7fc50..dc1b0c61f 100644 --- a/client/geoword/leaderboard.js +++ b/client/geoword/leaderboard.js @@ -5,7 +5,7 @@ const packetTitle = titleCase(packetName); document.getElementById('packet-name').textContent = packetTitle; document.getElementById('division').textContent = division; -fetch('/geoword/api/leaderboard?' + new URLSearchParams({ packetName, division })) +fetch('/api/geoword/leaderboard?' + new URLSearchParams({ packetName, division })) .then(response => response.json()) .then(data => { const { leaderboard } = data; diff --git a/client/geoword/stats.js b/client/geoword/stats.js index 5bcf43c89..c5483d319 100644 --- a/client/geoword/stats.js +++ b/client/geoword/stats.js @@ -3,7 +3,7 @@ const packetTitle = titleCase(packetName); document.getElementById('packet-name').textContent = packetTitle; -fetch('/geoword/api/stats?' + new URLSearchParams({ packetName })) +fetch('/api/geoword/stats?' + new URLSearchParams({ packetName })) .then(response => response.json()) .then(data => { const { buzzArray, division, leaderboard } = data; @@ -46,7 +46,7 @@ fetch('/geoword/api/stats?' + new URLSearchParams({ packetName })) }); -fetch('/geoword/api/get-divisions?' + new URLSearchParams({ packetName })) +fetch('/api/geoword/get-divisions?' + new URLSearchParams({ packetName })) .then(response => response.json()) .then(data => { const { divisions } = data; diff --git a/database/users.js b/database/users.js index 820dc3266..4756aedfa 100644 --- a/database/users.js +++ b/database/users.js @@ -316,6 +316,12 @@ async function getUserId(username) { } +async function isAdmin(username) { + const user = await getUser(username); + return user?.admin ?? false; +} + + async function recordBonusData(username, data) { const user_id = await getUserId(username); const newData = {}; @@ -428,6 +434,7 @@ export { getUsername, getUserField, getUserId, + isAdmin, recordBonusData, recordTossupData, updateUser, diff --git a/routes/api.js b/routes/api.js deleted file mode 100644 index 62ffc203b..000000000 --- a/routes/api.js +++ /dev/null @@ -1,253 +0,0 @@ -import { DEFAULT_QUERY_RETURN_LENGTH } from '../constants.js'; -import { getNumPackets, getPacket, getQuery, getRandomName, getRandomBonuses, getRandomTossups, reportQuestion, getSetList } from '../database/questions.js'; -import checkAnswer from '../server/checkAnswer.js'; -import { tossupRooms } from '../server/TossupRoom.js'; - -import { Router } from 'express'; -import rateLimit from 'express-rate-limit'; - - -const router = Router(); -// Apply the rate limiting middleware to API calls only -router.use(rateLimit({ - windowMs: 1000, // 4 seconds - max: 20, // Limit each IP to 20 requests per `window` - standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false, // Disable the `X-RateLimit-*` headers -})); - - -// express encodes same parameter passed multiple times as an array -// this middleware converts it to a single value -router.use((req, _res, next) => { - for (const key in req.query) { - if (Array.isArray(req.query[key])) { - req.query[key] = req.query[key][0]; - } - } - next(); -}); - - -router.get('/check-answer', (req, res) => { - const { answerline, givenAnswer } = req.query; - const { directive, directedPrompt } = checkAnswer(answerline, givenAnswer); - res.send(JSON.stringify({ directive: directive, directedPrompt: directedPrompt })); -}); - - -router.get('/num-packets', async (req, res) => { - const numPackets = await getNumPackets(req.query.setName); - if (numPackets === 0) { - res.statusCode = 404; - } - res.send(JSON.stringify({ numPackets: numPackets })); -}); - - -router.get('/packet', async (req, res) => { - const setName = req.query.setName; - const packetNumber = parseInt(req.query.packetNumber); - const packet = await getPacket({ setName, packetNumber }); - if (packet.tossups.length === 0 && packet.bonuses.length === 0) { - res.statusCode = 404; - } - res.send(JSON.stringify(packet)); -}); - - -router.get('/packet-bonuses', async (req, res) => { - const setName = req.query.setName; - const packetNumber = parseInt(req.query.packetNumber); - const packet = await getPacket({ setName, packetNumber, questionTypes: ['bonuses'] }); - if (packet.bonuses.length === 0) { - res.statusCode = 404; - } - res.send(JSON.stringify(packet)); -}); - - -router.get('/packet-tossups', async (req, res) => { - const setName = req.query.setName; - const packetNumber = parseInt(req.query.packetNumber); - const packet = await getPacket({ setName, packetNumber, questionTypes: ['tossups'] }); - if (packet.tossups.length === 0) { - res.statusCode = 404; - } - res.send(JSON.stringify(packet)); -}); - - -router.get('/query', async (req, res) => { - req.query.randomize = (req.query.randomize === 'true'); - req.query.regex = (req.query.regex === 'true'); - req.query.exactPhrase = (req.query.exactPhrase === 'true'); - req.query.ignoreDiacritics = (req.query.ignoreDiacritics === 'true'); - - if (!['tossup', 'bonus', 'all'].includes(req.query.questionType)) { - res.status(400).send('Invalid question type specified.'); - return; - } - - if (!['all', 'question', 'answer'].includes(req.query.searchType)) { - res.status(400).send('Invalid search type specified.'); - return; - } - - if (req.query.difficulties) { - req.query.difficulties = req.query.difficulties - .split(',') - .map((difficulty) => parseInt(difficulty)); - } - - if (req.query.categories) { - req.query.categories = req.query.categories.split(','); - } - - if (req.query.subcategories) { - req.query.subcategories = req.query.subcategories.split(','); - } - - if (!req.query.tossupPagination) { - req.query.tossupPagination = 1; - } - - if (!req.query.bonusPagination) { - req.query.bonusPagination = 1; - } - - if (!isFinite(req.query.tossupPagination) || !isFinite(req.query.bonusPagination)) { - res.status(400).send('Invalid pagination specified.'); - return; - } - - if (!req.query.maxReturnLength || isNaN(req.query.maxReturnLength)) { - req.query.maxReturnLength = DEFAULT_QUERY_RETURN_LENGTH; - } - - const maxPagination = Math.floor(4000 / (req.query.maxReturnLength || 25)); - - // bound pagination between 1 and maxPagination - req.query.tossupPagination = Math.min(parseInt(req.query.tossupPagination), maxPagination); - req.query.bonusPagination = Math.min(parseInt(req.query.bonusPagination), maxPagination); - req.query.tossupPagination = Math.max(req.query.tossupPagination, 1); - req.query.bonusPagination = Math.max(req.query.bonusPagination, 1); - - req.query.minYear = isNaN(req.query.minYear) ? undefined : parseInt(req.query.minYear); - req.query.maxYear = isNaN(req.query.maxYear) ? undefined : parseInt(req.query.maxYear); - - const queryResult = await getQuery(req.query); - res.send(JSON.stringify(queryResult)); -}); - - -router.get('/random-name', (req, res) => { - const randomName = getRandomName(); - res.send(JSON.stringify({ randomName: randomName })); -}); - - -router.get('/random-bonus', async (req, res) => { - if (req.query.difficulties) { - req.query.difficulties = req.query.difficulties - .split(',') - .map((difficulty) => parseInt(difficulty)); - - req.query.difficulties = req.query.difficulties.length ? req.query.difficulties : undefined; - } - - if (req.query.categories) { - req.query.categories = req.query.categories.split(','); - req.query.categories = req.query.categories.length ? req.query.categories : undefined; - } - - if (req.query.subcategories) { - req.query.subcategories = req.query.subcategories.split(','); - req.query.subcategories = req.query.subcategories.length ? req.query.subcategories : undefined; - } - - req.query.bonusLength = (req.query.threePartBonuses === 'true') ? 3 : undefined; - - req.query.minYear = isNaN(req.query.minYear) ? undefined : parseInt(req.query.minYear); - req.query.maxYear = isNaN(req.query.maxYear) ? undefined : parseInt(req.query.maxYear); - req.query.number = isNaN(req.query.number) ? undefined : parseInt(req.query.number); - - const bonuses = await getRandomBonuses(req.query); - if (bonuses.length === 0) { - res.status(404); - } - res.send(JSON.stringify({ bonuses: bonuses })); -}); - - -router.get('/random-tossup', async (req, res) => { - if (req.query.difficulties) { - req.query.difficulties = req.query.difficulties - .split(',') - .map((difficulty) => parseInt(difficulty)); - - req.query.difficulties = req.query.difficulties.length ? req.query.difficulties : undefined; - } - - if (req.query.categories) { - req.query.categories = req.query.categories.split(','); - req.query.categories = req.query.categories.length ? req.query.categories : undefined; - } - - if (req.query.subcategories) { - req.query.subcategories = req.query.subcategories.split(','); - req.query.subcategories = req.query.subcategories.length ? req.query.subcategories : undefined; - } - - req.query.minYear = isNaN(req.query.minYear) ? undefined : parseInt(req.query.minYear); - req.query.maxYear = isNaN(req.query.maxYear) ? undefined : parseInt(req.query.maxYear); - req.query.number = isNaN(req.query.number) ? undefined : parseInt(req.query.number); - - const tossups = await getRandomTossups(req.query); - if (tossups.length === 0) { - res.status(404); - } - - res.send(JSON.stringify({ tossups: tossups })); -}); - - -router.post('/report-question', async (req, res) => { - const _id = req.body._id; - const reason = req.body.reason ?? ''; - const description = req.body.description ?? ''; - const successful = await reportQuestion(_id, reason, description); - if (successful) { - res.sendStatus(200); - } else { - res.sendStatus(500); - } -}); - - -router.get('/set-list', (req, res) => { - const setList = getSetList(); - res.send(JSON.stringify({ setList })); -}); - - -router.get('/multiplayer/room-list', (_req, res) => { - const roomList = []; - for (const roomName in tossupRooms) { - if (!tossupRooms[roomName].settings.public) { - continue; - } - - roomList.push({ - roomName: roomName, - playerCount: Object.keys(tossupRooms[roomName].players).length, - onlineCount: Object.keys(tossupRooms[roomName].sockets).length, - isPermanent: tossupRooms[roomName].isPermanent, - }); - } - - res.send(JSON.stringify({ roomList: roomList })); -}); - - -export default router; diff --git a/routes/api/geoword.js b/routes/api/geoword.js new file mode 100644 index 000000000..3d7fd0b7d --- /dev/null +++ b/routes/api/geoword.js @@ -0,0 +1,127 @@ +import * as geoword from '../../database/geoword.js'; +import { getUserId } from '../../database/users.js'; +import { checkToken } from '../../server/authentication.js'; +import checkAnswer from '../../server/checkAnswer.js'; + +import { Router } from 'express'; + +const router = Router(); + +router.get('/check-answer', async (req, res) => { + const { givenAnswer, questionNumber, packetName } = req.query; + const answer = await geoword.getAnswer(packetName, parseInt(questionNumber)); + const { directive, directedPrompt } = checkAnswer(answer, givenAnswer); + res.json({ actualAnswer: answer, directive, directedPrompt }); +}); + +router.get('/get-progress', async (req, res) => { + const { username, token } = req.session; + if (!checkToken(username, token)) { + delete req.session; + res.redirect('/geoword/login'); + return; + } + + const packetName = req.query.packetName; + const { division, numberCorrect, points, totalCorrectCelerity, tossupsHeard } = await geoword.getProgress(packetName, username); + + res.json({ division, numberCorrect, points, totalCorrectCelerity, tossupsHeard }); +}); + +router.get('/get-divisions', async (req, res) => { + const divisions = await geoword.getDivisions(req.query.packetName); + res.json({ divisions }); +}); + +router.get('/leaderboard', async (req, res) => { + const { packetName, division } = req.query; + const leaderboard = await geoword.getLeaderboard(packetName, division); + res.json({ leaderboard }); +}); + +router.get('/packet-list', async (req, res) => { + const packetList = await geoword.getPacketList(); + res.json({ packetList }); +}); + +router.get('/get-question-count', async (req, res) => { + const { packetName } = req.query; + const questionCount = await geoword.getQuestionCount(packetName); + res.json({ questionCount }); +}); + +router.put('/record-division', async (req, res) => { + const { username, token } = req.session; + if (!checkToken(username, token)) { + delete req.session; + res.sendStatus(401); + return; + } + + const { packetName, division } = req.body; + const result = await geoword.recordDivision({ packetName, username, division }); + + if (result) { + res.sendStatus(200); + } else { + res.sendStatus(500); + } +}); + +router.put('/record-protest', async (req, res) => { + const { username, token } = req.session; + if (!checkToken(username, token)) { + delete req.session; + res.sendStatus(401); + return; + } + + const { packetName, questionNumber } = req.body; + const result = await geoword.recordProtest({ packetName, questionNumber, username }); + + if (result) { + res.sendStatus(200); + } else { + res.sendStatus(500); + } +}); + + +router.get('/stats', 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.query; + const { buzzArray, division, leaderboard } = await geoword.getUserStats({ packetName, user_id }); + 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/index.js b/routes/api/index.js new file mode 100644 index 000000000..69f2bf62a --- /dev/null +++ b/routes/api/index.js @@ -0,0 +1,111 @@ +import { getNumPackets, getPacket, getRandomName, reportQuestion, getSetList } from '../../database/questions.js'; +import checkAnswer from '../../server/checkAnswer.js'; + +import geowordRouter from './geoword.js'; +import multiplayerRouter from './multiplayer.js'; +import queryRouter from './query.js'; +import randomBonusRouter from './random-bonus.js'; +import randomTossupRouter from './random-tossup.js'; + +import { Router } from 'express'; +import rateLimit from 'express-rate-limit'; + + +const router = Router(); +// Apply the rate limiting middleware to API calls only +router.use(rateLimit({ + windowMs: 1000, // 4 seconds + max: 20, // Limit each IP to 20 requests per `window` + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers +})); + + +// express encodes same parameter passed multiple times as an array +// this middleware converts it to a single value +router.use((req, _res, next) => { + for (const key in req.query) { + if (Array.isArray(req.query[key])) { + req.query[key] = req.query[key][0]; + } + } + next(); +}); + +router.get('/check-answer', (req, res) => { + const { answerline, givenAnswer } = req.query; + const { directive, directedPrompt } = checkAnswer(answerline, givenAnswer); + res.send(JSON.stringify({ directive: directive, directedPrompt: directedPrompt })); +}); + +router.use('/geoword', geowordRouter); + +router.use('/multiplayer', multiplayerRouter); + +router.get('/num-packets', async (req, res) => { + const numPackets = await getNumPackets(req.query.setName); + if (numPackets === 0) { + res.statusCode = 404; + } + res.send(JSON.stringify({ numPackets: numPackets })); +}); + +router.get('/packet', async (req, res) => { + const setName = req.query.setName; + const packetNumber = parseInt(req.query.packetNumber); + const packet = await getPacket({ setName, packetNumber }); + if (packet.tossups.length === 0 && packet.bonuses.length === 0) { + res.statusCode = 404; + } + res.send(JSON.stringify(packet)); +}); + +router.get('/packet-bonuses', async (req, res) => { + const setName = req.query.setName; + const packetNumber = parseInt(req.query.packetNumber); + const packet = await getPacket({ setName, packetNumber, questionTypes: ['bonuses'] }); + if (packet.bonuses.length === 0) { + res.statusCode = 404; + } + res.send(JSON.stringify(packet)); +}); + +router.get('/packet-tossups', async (req, res) => { + const setName = req.query.setName; + const packetNumber = parseInt(req.query.packetNumber); + const packet = await getPacket({ setName, packetNumber, questionTypes: ['tossups'] }); + if (packet.tossups.length === 0) { + res.statusCode = 404; + } + res.send(JSON.stringify(packet)); +}); + +router.use('/query', queryRouter); + +router.get('/random-name', (req, res) => { + const randomName = getRandomName(); + res.send(JSON.stringify({ randomName: randomName })); +}); + +router.use('/random-bonus', randomBonusRouter); + +router.use('/random-tossup', randomTossupRouter); + +router.post('/report-question', async (req, res) => { + const _id = req.body._id; + const reason = req.body.reason ?? ''; + const description = req.body.description ?? ''; + const successful = await reportQuestion(_id, reason, description); + if (successful) { + res.sendStatus(200); + } else { + res.sendStatus(500); + } +}); + +router.get('/set-list', (req, res) => { + const setList = getSetList(); + res.send(JSON.stringify({ setList })); +}); + +export default router; diff --git a/routes/api/multiplayer.js b/routes/api/multiplayer.js new file mode 100644 index 000000000..f7ebfacfb --- /dev/null +++ b/routes/api/multiplayer.js @@ -0,0 +1,26 @@ +import { tossupRooms } from '../../server/TossupRoom.js'; + +import { Router } from 'express'; +const router = Router(); + + +router.get('/room-list', (req, res) => { + const roomList = []; + for (const roomName in tossupRooms) { + if (!tossupRooms[roomName].settings.public) { + continue; + } + + roomList.push({ + roomName: roomName, + playerCount: Object.keys(tossupRooms[roomName].players).length, + onlineCount: Object.keys(tossupRooms[roomName].sockets).length, + isPermanent: tossupRooms[roomName].isPermanent, + }); + } + + res.send(JSON.stringify({ roomList: roomList })); +}); + + +export default router; diff --git a/routes/api/query.js b/routes/api/query.js new file mode 100644 index 000000000..aaf19d1d2 --- /dev/null +++ b/routes/api/query.js @@ -0,0 +1,71 @@ +import { DEFAULT_QUERY_RETURN_LENGTH } from '../../constants.js'; +import { getQuery } from '../../database/questions.js'; + +import { Router } from 'express'; +const router = Router(); + + +router.get('/', async (req, res) => { + req.query.randomize = (req.query.randomize === 'true'); + req.query.regex = (req.query.regex === 'true'); + req.query.exactPhrase = (req.query.exactPhrase === 'true'); + req.query.ignoreDiacritics = (req.query.ignoreDiacritics === 'true'); + + if (!['tossup', 'bonus', 'all'].includes(req.query.questionType)) { + res.status(400).send('Invalid question type specified.'); + return; + } + + if (!['all', 'question', 'answer'].includes(req.query.searchType)) { + res.status(400).send('Invalid search type specified.'); + return; + } + + if (req.query.difficulties) { + req.query.difficulties = req.query.difficulties + .split(',') + .map((difficulty) => parseInt(difficulty)); + } + + if (req.query.categories) { + req.query.categories = req.query.categories.split(','); + } + + if (req.query.subcategories) { + req.query.subcategories = req.query.subcategories.split(','); + } + + if (!req.query.tossupPagination) { + req.query.tossupPagination = 1; + } + + if (!req.query.bonusPagination) { + req.query.bonusPagination = 1; + } + + if (!isFinite(req.query.tossupPagination) || !isFinite(req.query.bonusPagination)) { + res.status(400).send('Invalid pagination specified.'); + return; + } + + if (!req.query.maxReturnLength || isNaN(req.query.maxReturnLength)) { + req.query.maxReturnLength = DEFAULT_QUERY_RETURN_LENGTH; + } + + const maxPagination = Math.floor(4000 / (req.query.maxReturnLength || 25)); + + // bound pagination between 1 and maxPagination + req.query.tossupPagination = Math.min(parseInt(req.query.tossupPagination), maxPagination); + req.query.bonusPagination = Math.min(parseInt(req.query.bonusPagination), maxPagination); + req.query.tossupPagination = Math.max(req.query.tossupPagination, 1); + req.query.bonusPagination = Math.max(req.query.bonusPagination, 1); + + req.query.minYear = isNaN(req.query.minYear) ? undefined : parseInt(req.query.minYear); + req.query.maxYear = isNaN(req.query.maxYear) ? undefined : parseInt(req.query.maxYear); + + const queryResult = await getQuery(req.query); + res.send(JSON.stringify(queryResult)); +}); + + +export default router; diff --git a/routes/api/random-bonus.js b/routes/api/random-bonus.js new file mode 100644 index 000000000..dc5559bab --- /dev/null +++ b/routes/api/random-bonus.js @@ -0,0 +1,41 @@ +import { getRandomBonuses } from '../../database/questions.js'; + +import { Router } from 'express'; +const router = Router(); + + +router.get('/', async (req, res) => { + if (req.query.difficulties) { + req.query.difficulties = req.query.difficulties + .split(',') + .map((difficulty) => parseInt(difficulty)); + + req.query.difficulties = req.query.difficulties.length ? req.query.difficulties : undefined; + } + + if (req.query.categories) { + req.query.categories = req.query.categories.split(','); + req.query.categories = req.query.categories.length ? req.query.categories : undefined; + } + + if (req.query.subcategories) { + req.query.subcategories = req.query.subcategories.split(','); + req.query.subcategories = req.query.subcategories.length ? req.query.subcategories : undefined; + } + + req.query.bonusLength = (req.query.threePartBonuses === 'true') ? 3 : undefined; + + req.query.minYear = isNaN(req.query.minYear) ? undefined : parseInt(req.query.minYear); + req.query.maxYear = isNaN(req.query.maxYear) ? undefined : parseInt(req.query.maxYear); + req.query.number = isNaN(req.query.number) ? undefined : parseInt(req.query.number); + + const bonuses = await getRandomBonuses(req.query); + + if (bonuses.length === 0) { + res.status(404); + } + + res.json({ bonuses }); +}); + +export default router; diff --git a/routes/api/random-tossup.js b/routes/api/random-tossup.js new file mode 100644 index 000000000..fdaf75b61 --- /dev/null +++ b/routes/api/random-tossup.js @@ -0,0 +1,39 @@ +import { getRandomTossups } from '../../database/questions.js'; + +import { Router } from 'express'; +const router = Router(); + + +router.get('/', async (req, res) => { + if (req.query.difficulties) { + req.query.difficulties = req.query.difficulties + .split(',') + .map((difficulty) => parseInt(difficulty)); + + req.query.difficulties = req.query.difficulties.length ? req.query.difficulties : undefined; + } + + if (req.query.categories) { + req.query.categories = req.query.categories.split(','); + req.query.categories = req.query.categories.length ? req.query.categories : undefined; + } + + if (req.query.subcategories) { + req.query.subcategories = req.query.subcategories.split(','); + req.query.subcategories = req.query.subcategories.length ? req.query.subcategories : undefined; + } + + req.query.minYear = isNaN(req.query.minYear) ? undefined : parseInt(req.query.minYear); + req.query.maxYear = isNaN(req.query.maxYear) ? undefined : parseInt(req.query.maxYear); + req.query.number = isNaN(req.query.number) ? undefined : parseInt(req.query.number); + + const tossups = await getRandomTossups(req.query); + + if (tossups.length === 0) { + res.status(404); + } + + res.json({ tossups }); +}); + +export default router; diff --git a/routes/geoword.js b/routes/geoword.js index 3d34850d1..9ef77959a 100644 --- a/routes/geoword.js +++ b/routes/geoword.js @@ -1,7 +1,5 @@ import * as geoword from '../database/geoword.js'; -import { getUserId } from '../database/users.js'; import { checkToken } from '../server/authentication.js'; -import checkAnswer from '../server/checkAnswer.js'; import { Router } from 'express'; @@ -97,121 +95,4 @@ router.get('/stats/:packetName', (req, res) => { res.sendFile('stats.html', { root: './client/geoword' }); }); -router.get('/api/check-answer', async (req, res) => { - const { givenAnswer, questionNumber, packetName } = req.query; - const answer = await geoword.getAnswer(packetName, parseInt(questionNumber)); - const { directive, directedPrompt } = checkAnswer(answer, givenAnswer); - res.json({ actualAnswer: answer, directive, directedPrompt }); -}); - -router.get('/api/get-progress', async (req, res) => { - const { username, token } = req.session; - if (!checkToken(username, token)) { - delete req.session; - res.redirect('/geoword/login'); - return; - } - - const packetName = req.query.packetName; - const { division, numberCorrect, points, totalCorrectCelerity, tossupsHeard } = await geoword.getProgress(packetName, username); - - res.json({ division, numberCorrect, points, totalCorrectCelerity, tossupsHeard }); -}); - -router.get('/api/get-divisions', async (req, res) => { - const divisions = await geoword.getDivisions(req.query.packetName); - res.json({ divisions }); -}); - -router.get('/api/leaderboard', async (req, res) => { - const { packetName, division } = req.query; - const leaderboard = await geoword.getLeaderboard(packetName, division); - res.json({ leaderboard }); -}); - -router.get('/api/packet-list', async (req, res) => { - const packetList = await geoword.getPacketList(); - res.json({ packetList }); -}); - -router.get('/api/get-question-count', async (req, res) => { - const { packetName } = req.query; - const questionCount = await geoword.getQuestionCount(packetName); - res.json({ questionCount }); -}); - -router.put('/api/record-division', async (req, res) => { - const { username, token } = req.session; - if (!checkToken(username, token)) { - delete req.session; - res.sendStatus(401); - return; - } - - const { packetName, division } = req.body; - const result = await geoword.recordDivision({ packetName, username, division }); - - if (result) { - res.sendStatus(200); - } else { - res.sendStatus(500); - } -}); - -router.put('/api/record-protest', async (req, res) => { - const { username, token } = req.session; - if (!checkToken(username, token)) { - delete req.session; - res.sendStatus(401); - return; - } - - const { packetName, questionNumber } = req.body; - const result = await geoword.recordProtest({ packetName, questionNumber, username }); - - if (result) { - res.sendStatus(200); - } else { - res.sendStatus(500); - } -}); - - -router.get('/api/stats', 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.query; - const { buzzArray, division, leaderboard } = await geoword.getUserStats({ packetName, user_id }); - res.json({ buzzArray, division, leaderboard }); -}); - -router.get('/api/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/server/server.js b/server/server.js index 084ca556d..57d31c9dd 100644 --- a/server/server.js +++ b/server/server.js @@ -4,7 +4,7 @@ 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.js'; +import apiRouter from '../routes/api/index.js'; import apiDocsRouter from '../routes/api-docs.js'; import authRouter from '../routes/auth.js'; import backupsRouter from '../routes/backups.js';