/* ---------------------------------------------------------------------------- Question Server GitLab: This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ------------------------------------------------------------------------- */ // package requires const express = require('express') const bodyParser = require('body-parser') const busboy = require('connect-busboy') const { Worker } = require('worker_threads') const { v4: uuidv4 } = require('uuid') const fs = require('fs') const app = express() // other requires const logger = require('../../utils/logger.js') const utils = require('../../utils/utils.js') const actions = require('../../utils/actions.js') const dbtools = require('../../utils/dbtools.js') const auth = require('../../middlewares/auth.middleware.js') const { searchData } = require('../../utils/classes.js') // files const searchDataWorkerFile = './src/utils/searchData.js' const msgFile = 'stats/msgs' const passwordFile = 'data/dataEditorPasswords.json' const dataEditsLog = 'stats/dataEdits' const dailyDataCountFile = 'stats/dailyDataCount' const usersDbBackupPath = 'data/dbs/backup' const quickVoteResultsDir = 'stats/qvote' const quickVotes = 'stats/qvote/votes.json' const testUsersFile = 'data/testUsers.json' // other constants const maxVeteranPwGetCount = 10 const addPWPerDay = 3 // every x day a user can give a pw const maxPWCount = 6 // maximum pw give opportunities a user can have at once // const daysAfterUserGetsPWs = 2 // days after user gets pw-s // stuff gotten from server.js let userDB let url // eslint-disable-line let publicdirs = [] function GetApp() { const publicDir = publicdirs[0] if (!publicDir) { throw new Error(`No public dir! ( API )`) } // files in public dirs const recivedFiles = publicDir + 'recivedfiles' const uloadFiles = publicDir + 'f' const dataFile = publicDir + 'data.json' const motdFile = publicDir + 'motd' const versionFile = publicDir + 'version' app.use( bodyParser.urlencoded({ limit: '10mb', extended: true, }) ) app.use( bodyParser.json({ limit: '10mb', }) ) app.set('view engine', 'ejs') app.set('views', ['./modules/api/views', './sharedViews']) app.use( auth({ userDB: userDB, jsonResponse: true, exceptions: [ '/favicon.ico', '/login', '/getveteranpw', '/postfeedbackfile', '/postfeedback', '/fosuploader', '/badtestsender', ], }) ) publicdirs.forEach((pdir) => { logger.Log(`Using public dir: ${pdir}`) app.use(express.static(pdir)) }) app.use( busboy({ limits: { fileSize: 50000 * 1024 * 1024, }, }) ) var data = actions.LoadJSON(dataFile) var version = '' var motd = '' var testUsers = [] function LoadVersion() { version = utils.ReadFile(versionFile) } function LoadMOTD() { motd = utils.ReadFile(motdFile) } function LoadTestUsers() { testUsers = utils.ReadJSON(testUsersFile) if (testUsers) { testUsers = testUsers.userIds } } function Load() { utils.WatchFile(motdFile, (newData) => { logger.Log(`Motd changed: ${newData.replace(/\/n/g, '')}`) LoadMOTD() }) utils.WatchFile(versionFile, (newData) => { logger.Log(`Version changed: ${newData.replace(/\/n/g, '')}`) LoadVersion() }) utils.WatchFile(testUsersFile, (newData) => { logger.Log(`Test Users file changed: ${newData.replace(/\/n/g, '')}`) LoadTestUsers() }) LoadTestUsers() LoadVersion() LoadMOTD() } Load() // ------------------------------------------------------------- app.get('/quickvote', (req, res) => { const key = req.query.key const val = req.query.val const user = req.session.user if (!key || !val) { res.render('votethank', { results: 'error', msg: 'no key or val query param!', }) return } let votes = {} if (utils.FileExists(quickVotes)) { votes = utils.ReadJSON(quickVotes) } else { logger.Log( `No such vote "${key}", and quickVotes.json is missing ( #${user.id}: ${key}-${val} )`, logger.GetColor('blue') ) res.render('votethank', { result: 'no such pool', }) return } if (!votes.voteNames.includes(key)) { logger.Log( `No such vote "${key}" ( #${user.id}: ${key}-${val} )`, logger.GetColor('blue') ) res.render('votethank', { result: 'no such pool', }) return } const voteFile = quickVoteResultsDir + '/' + key + '.json' let voteData = { votes: {}, users: [], } if (utils.FileExists(voteFile)) { voteData = utils.ReadJSON(voteFile) } else { utils.CreatePath(quickVoteResultsDir) } if (!voteData.users.includes(user.id)) { if (voteData.votes[val]) { voteData.votes[val]++ } else { voteData.votes[val] = 1 } voteData.users.push(user.id) logger.Log( `Vote from #${user.id}: ${key}: ${val}`, logger.GetColor('blue') ) res.render('votethank', { result: 'success', msg: 'vote added', }) } else { logger.Log( `#${user.id} already voted for: ${key}: ${val}`, logger.GetColor('blue') ) res.render('votethank', { result: 'already voted', msg: 'already voted', }) } utils.WriteFile(JSON.stringify(voteData), voteFile) }) app.get('/avaiblePWS', (req, res) => { logger.LogReq(req) const user = req.session.user res.json({ result: 'success', userCreated: user.created, avaiblePWS: user.avaiblePWRequests, requestedPWS: user.pwRequestCount, maxPWCount: maxPWCount, // daysAfterUserGetsPWs: daysAfterUserGetsPWs, addPWPerDay: addPWPerDay, }) }) app.post('/getpw', function(req, res) { logger.LogReq(req) const requestingUser = req.session.user if (requestingUser.avaiblePWRequests <= 0) { res.json({ result: 'error', msg: 'Too many passwords requested or cant request password yet, try later', }) logger.Log( `User #${requestingUser.id} requested too much passwords`, logger.GetColor('cyan') ) return } dbtools.Update( userDB, 'users', { avaiblePWRequests: requestingUser.avaiblePWRequests - 1, pwRequestCount: requestingUser.pwRequestCount + 1, }, { id: requestingUser.id, } ) const pw = uuidv4() const insertRes = dbtools.Insert(userDB, 'users', { pw: pw, avaiblePWRequests: 0, created: utils.GetDateString(), }) logger.Log( `User #${requestingUser.id} created new user #${insertRes.lastInsertRowid}`, logger.GetColor('cyan') ) res.json({ result: 'success', pw: pw, requestedPWS: requestingUser.pwRequestCount + 1, remaining: requestingUser.avaiblePWRequests - 1, }) }) app.post('/getveteranpw', function(req, res) { logger.LogReq(req) const ip = req.headers['cf-connecting-ip'] || req.connection.remoteAddress const tries = dbtools.Select(userDB, 'veteranPWRequests', { ip: ip, })[0] if (tries) { if (tries.count > maxVeteranPwGetCount) { res.json({ result: 'error', msg: 'Too many tries from this IP', }) logger.Log( `Too many veteran PW requests from ${ip}!`, logger.GetColor('cyan') ) return } else { dbtools.Update( userDB, 'veteranPWRequests', { count: tries.count + 1, lastDate: utils.GetDateString(), }, { id: tries.id, } ) } } else { dbtools.Insert(userDB, 'veteranPWRequests', { ip: ip, lastDate: utils.GetDateString(), }) } const oldUserID = req.body.cid if (!oldUserID) { res.json({ result: 'error', msg: 'No Client ID recieved', }) logger.Log(`No client ID recieved`, logger.GetColor('cyan')) return } const user = dbtools.Select(userDB, 'users', { oldCID: oldUserID, })[0] if (user) { if (user.pwGotFromCID === 0) { logger.Log( `Sent password to veteran user #${user.id}`, logger.GetColor('cyan') ) dbtools.Update( userDB, 'users', { pwGotFromCID: 1, }, { id: user.id, } ) res.json({ result: 'success', pw: user.pw, }) } else { logger.Log( `Veteran user #${user.id} already requested password`, logger.GetColor('cyan') ) res.json({ result: 'error', msg: 'Password already requested', }) } } else { logger.Log( `Invalid password request with CID: ${oldUserID}`, logger.GetColor('cyan') ) res.json({ result: 'error', msg: 'No such Client ID', }) } }) app.post('/login', (req, res) => { logger.LogReq(req) const pw = req.body.pw || false const cid = req.body.cid const isScript = req.body.script const ip = req.headers['cf-connecting-ip'] || req.connection.remoteAddress const user = dbtools.Select(userDB, 'users', { pw: pw, })[0] if (user) { const sessionID = uuidv4() // FIXME: Users now can only log in in one session, this might be too strict. const existingSessions = dbtools.Select(userDB, 'sessions', { userID: user.id, isScript: isScript ? 1 : 0, }) if (existingSessions.length > 0) { logger.Log( `Multiple ${isScript ? 'script' : 'website'} sessions ( ${ existingSessions.length } ) for #${user.id}, deleting olds`, logger.GetColor('cyan') ) existingSessions.forEach((sess) => { dbtools.Delete(userDB, 'sessions', { id: sess.id, isScript: isScript ? 1 : 0, }) }) } dbtools.Update( userDB, 'users', { loginCount: user.loginCount + 1, lastIP: ip, lastLogin: utils.GetDateString(), }, { id: user.id, } ) dbtools.Insert(userDB, 'sessions', { id: sessionID, ip: ip, userID: user.id, isScript: isScript ? 1 : 0, createDate: utils.GetDateString(), }) // https://www.npmjs.com/package/cookie // TODO: cookie age res.cookie('sessionID', sessionID, { domain: '.frylabs.net', // TODO: use url. url: "https://api.frylabs.net" expires: new Date( new Date().getTime() + 10 * 365 * 24 * 60 * 60 * 1000 ), sameSite: 'none', secure: true, }) res.cookie('sessionID', sessionID, { expires: new Date( new Date().getTime() + 10 * 365 * 24 * 60 * 60 * 1000 ), sameSite: 'none', secure: true, }) res.json({ result: 'success', msg: 'you are now logged in', }) logger.Log( `Successfull login to ${ isScript ? 'script' : 'website' } with user ID: #${user.id}`, logger.GetColor('cyan') ) } else { logger.Log( `Login attempt with invalid pw: ${pw} to ${ isScript ? 'script' : 'website' }${cid ? ', CID:' + cid : ''}`, logger.GetColor('cyan') ) res.json({ result: 'error', msg: 'Invalid password', }) } }) app.post('/logout', (req, res) => { logger.LogReq(req) const sessionID = req.cookies.sessionID // removing session from db dbtools.Delete(userDB, 'sessions', { id: sessionID, }) // TODO: remove old sessions every once in a while res.clearCookie('sessionID').json({ result: 'success', }) }) // -------------------------------------------------------------- app.get('/', function(req, res) { logger.LogReq(req) res.redirect('https://www.youtube.com/watch?v=ieqGJgqiXFk') }) app.post('/postfeedbackfile', function(req, res) { UploadFile(req, res, uloadFiles, () => { res.json({ success: true }) }) logger.LogReq(req) logger.Log('New feedback file', logger.GetColor('bluebg'), true) }) app.post('/postfeedback', function(req, res) { logger.LogReq(req) if (req.body.fromLogin) { logger.Log( 'New feedback message from Login page', logger.GetColor('bluebg'), true ) } else { logger.Log( 'New feedback message from feedback page', logger.GetColor('bluebg'), true ) } const ip = req.headers['cf-connecting-ip'] || req.connection.remoteAddress const user = req.session.user utils.AppendToFile( utils.GetDateString() + ':\n' + JSON.stringify({ ...req.body, userID: user ? user.id : 'no user', ip: ip, }), msgFile ) res.json({ success: true }) }) function UploadFile(req, res, path, next) { try { var fstream req.pipe(req.busboy) req.busboy.on('file', function(fieldname, file, filename) { logger.Log('Uploading: ' + filename, logger.GetColor('blue')) utils.CreatePath(path, true) let date = new Date() let fn = date.getHours() + '' + date.getMinutes() + '' + date.getSeconds() + '_' + filename fstream = fs.createWriteStream(path + '/' + fn) file.pipe(fstream) fstream.on('close', function() { logger.Log( 'Upload Finished of ' + path + '/' + fn, logger.GetColor('blue') ) next(fn) }) fstream.on('error', function(err) { console.log(err) res.end('something bad happened :s') }) }) } catch (err) { logger.Log(`Unable to upload file!`, logger.GetColor('redbg')) console.log(err) } } app.route('/fosuploader').post(function(req, res) { UploadFile(req, res, uloadFiles, (fn) => { res.redirect('/f/' + fn) }) }) app.route('/badtestsender').post(function(req, res) { UploadFile(req, res, recivedFiles, () => { res.redirect('back') }) logger.LogReq(req) }) app.get('/allqr.txt', function(req, res) { res.set('Content-Type', 'text/plain') res.send(data.toString()) res.end() logger.LogReq(req) }) // ------------------------------------------------------------------------------------------- // API app.post('/uploaddata', (req, res) => { // body: JSON.stringify({ // newData: data, // count: getCount(data), // initialCount: initialCount, // password: password, // editedQuestions: editedQuestions // }) const { count, initialCount, editedQuestions, password, newData } = req.body const respStatuses = { invalidPass: 'invalidPass', ok: 'ok', error: 'error', } logger.LogReq(req) try { // finding user const pwds = JSON.parse(utils.ReadFile(passwordFile)) let user = Object.keys(pwds).find((key) => { const user = pwds[key] return user.password === password }) user = pwds[user] // logging and stuff logger.Log(`Data upload`, logger.GetColor('bluebg')) logger.Log(`PWD: ${password}`, logger.GetColor('bluebg')) // returning if user password is not ok if (!user) { logger.Log( `Data upload: invalid password ${password}`, logger.GetColor('red') ) utils.AppendToFile( utils.GetDateString() + '\n' + password + '(FAILED PASSWORD)\n' + JSON.stringify(editedQuestions) + '\n\n', dataEditsLog ) res.json({ status: respStatuses.invalidPass }) return } logger.Log( `Password accepted for ${user.name}`, logger.GetColor('bluebg') ) logger.Log( `Old Subjects/Questions: ${initialCount.subjectCount} / ${ initialCount.questionCount } | New: ${count.subjectCount} / ${ count.questionCount } | Edited question count: ${Object.keys(editedQuestions).length}`, logger.GetColor('bluebg') ) // saving detailed editedCount utils.AppendToFile( utils.GetDateString() + '\n' + JSON.stringify(user) + '\n' + JSON.stringify(editedQuestions) + '\n\n', dataEditsLog ) // making backup utils.CopyFile( './' + dataFile, `./qminingPublic/backs/data_before_${ user.name }_${utils.GetDateString().replace(/ /g, '_')}` ) // TODO: rewrite to dinamyc public!!! logger.Log('Backup made') // writing data utils.WriteFile(JSON.stringify(newData), dataFile) logger.Log('New data file written') // reloading data file data = actions.LoadJSON(dataFile) // data = newData logger.Log('Data set to newData') res.json({ status: respStatuses.ok, user: user.name, }) logger.Log('Data updating done!', logger.GetColor('bluebg')) } catch (error) { logger.Log(`Data upload error! `, logger.GetColor('redbg')) console.error(error) res.json({ status: respStatuses.error, msg: error.message }) } }) app.post('/isAdding', function(req, res) { logger.LogReq(req) const user = req.session.user const dryRun = testUsers.includes(user.id) // automatically saves to dataFile every n write // FIXME: req.body.datatoadd is for backwards compatibility, remove this sometime in the future actions .ProcessIncomingRequest( req.body.datatoadd || req.body, data, { motd, version }, dryRun ) .then((result) => { res.json({ success: result !== -1, newQuestions: result, }) }) }) app.get('/ask', function(req, res) { if (Object.keys(req.query).length === 0) { logger.DebugLog(`No query params`, 'ask', 1) res.json({ message: `ask something! ?q=[question]&subj=[subject]&data=[question data]. 'subj' is optimal for faster result`, result: [], recievedData: JSON.stringify(req.query), success: false, }) } else { if (req.query.q && req.query.data) { let subj = req.query.subj || '' let question = req.query.q let recData = {} try { recData = JSON.parse(req.query.data) } catch (error) { logger.Log( `Unable to parse recieved question data! '${req.query.data}'`, logger.GetColor('redbg') ) } // const worker = new Worker(searchDataWorkerFile, { // workerData: { // data, // question, // subj, // recData, // }, // }) // worker.on('error', (err) => { // logger.Log('Search Data Worker error!', logger.GetColor('redbg')) // console.error(err) // // TODO: handle error // }) // worker.on('exit', (code) => { // logger.DebugLog('Search Data exit, code: ' + code, 'actions', 1) // if (code !== 0) { // logger.Log( // 'Search Data Worker error! Exit code is not 0', // logger.GetColor('redbg') // ) // // TODO: handle error // } // }) // // let result = data.Search(question, subj, recData) // worker.on('message', (workerMsg) => { // const result = workerMsg // res.json({ // result: result, // success: true, // }) // logger.DebugLog(`Question result length: ${result.length}`, 'ask', 1) // logger.DebugLog(result, 'ask', 2) // }) let result = searchData(data, question, subj, recData) res.json({ result: result, success: true, }) logger.DebugLog(`Question result length: ${result.length}`, 'ask', 1) logger.DebugLog(result, 'ask', 2) } else { logger.DebugLog(`Invalid question`, 'ask', 1) res.json({ message: `Invalid question :(`, result: [], recievedData: JSON.stringify(req.query), success: false, }) } } }) function getSimplreRes() { return { subjects: data.length, questions: data.Subjects.reduce((acc, subj) => { return acc + subj.length }, 0), } } function getDetailedRes() { return data.Subjects.map((subj) => { return { name: subj.Name, count: subj.length, } }) } app.get('/datacount', function(req, res) { logger.LogReq(req) if (req.query.detailed === 'all') { res.json({ detailed: getDetailedRes(), simple: getSimplreRes(), }) } else if (req.query.detailed) { res.json(getDetailedRes()) } else { res.json(getSimplreRes()) } }) app.get('/infos', function(req, res) { const user = req.session.user let result = { result: 'success', uid: user.id, } if (req.query.subjinfo) { result.subjinfo = getSimplreRes() } if (req.query.version) { result.version = version } if (req.query.motd) { result.motd = motd } res.json(result) }) // ------------------------------------------------------------------------------------------- app.get('*', function(req, res) { res.status(404).render('404') }) app.post('*', function(req, res) { res.status(404).render('404') }) function ExportDailyDataCount() { logger.Log('Saving daily data count ...') utils.AppendToFile( JSON.stringify({ date: utils.GetDateString(), subjectCount: data.Subjects.length, questionCOunt: data.Subjects.reduce((acc, subj) => { return acc + subj.Questions.length }, 0), userCount: dbtools.TableInfo(userDB, 'users').dataCount, }), dailyDataCountFile ) } function BackupDB() { logger.Log('Backing up auth DB ...') utils.CreatePath(usersDbBackupPath, true) userDB .backup( `${usersDbBackupPath}/users.${utils .GetDateString() .replace(/ /g, '_')}.db` ) .then(() => { logger.Log('Auth DB backup complete!') }) .catch((err) => { logger.Log('Auth DB backup failed!', logger.GetColor('redbg')) console.error(err) }) } function IncrementAvaiblePWs() { // FIXME: check this if this is legit and works logger.Log('Incrementing avaible PW-s ...') const users = dbtools.SelectAll(userDB, 'users') const today = new Date() const getDayDiff = (dateString) => { let msdiff = today - new Date(dateString) return Math.floor(msdiff / (1000 * 3600 * 24)) } users.forEach((user) => { if (user.avaiblePWRequests >= maxPWCount) { return } const dayDiff = getDayDiff(user.created) // if (dayDiff < daysAfterUserGetsPWs) { // logger.Log(`User #${u.id} is not registered long enough to get password ( ${dayDiff} days, ${daysAfterUserGetsPWs} needed)`, logger.GetColor('cyan')) // return // } if (dayDiff % addPWPerDay === 0) { logger.Log( `Incrementing avaible PW-s for user #${user.id}: ${ user.avaiblePWRequests } -> ${user.avaiblePWRequests + 1}`, logger.GetColor('cyan') ) dbtools.Update( userDB, 'users', { avaiblePWRequests: user.avaiblePWRequests + 1, }, { id: user.id, } ) } }) } function DailyAction() { ExportDailyDataCount() BackupDB() IncrementAvaiblePWs() } return { dailyAction: DailyAction, app: app, } } exports.name = 'API' exports.getApp = GetApp exports.setup = (data) => { userDB = data.userDB url = data.url // eslint-disable-line publicdirs = data.publicdirs }