//Set up environmental variables, pulled from ~/.env file used as process.env.DB_HOST const os = require('os') //Used to get path of home directory const result = require('dotenv').config({ path:(os.homedir()+'/.env') }) const ports = { express: 3000, socketIo: 3001 } //Allow user of @ in in require calls. Config in package.json // Point module-alias at the repo root explicitly. The bare /register entry // locates package.json by walking up from its own directory, which lands // inside .pnpm/ under pnpm's symlinked store and fails to find it. require('module-alias')(require('path').join(__dirname, '..')) //Auth helper, used for decoding users web token let Auth = require('@helpers/Auth') //Note model, used to authorise socket room joins let Note = require('@models/Note') //Helmet adds additional security to express server const helmet = require('helmet') //Setup express server const express = require('express') const app = express() app.use( helmet() ) // allow for the parsing of url encoded forms app.use(express.urlencoded({ extended: true })); // // Request Rate Limiter // const rateLimit = require('express-rate-limit') //Limiter for the entire app const limiter = rateLimit({ windowMs: 10 * 60 * 1000, // 10 minutes max: 1000 // limit each IP to 1000 requests per windowMs }) // apply to all requests app.use(limiter); var http = require('http').createServer(app); var io = require('socket.io')(http, { path:'/socket' }); //Set socket IO as a global in the app global.SocketIo = io let noteDiffs = {} // // Room names are namespaced. Previously note rooms were keyed on the bare // note_raw_text.id and user rooms on the bare user.id, sharing one flat // keyspace -- so user 7 and note_raw_text 7 were the same room. // const userRoom = userId => `user:${userId}` const noteRoom = rawTextId => `note:${rawTextId}` io.on('connection', function(socket){ //When a user connects, add them to their own room // This allows the server to emit events to that specific user // access socket.io in the controller with SocketIo global socket.on('user_connect', token => { Auth.decodeToken(token) .then(userData => { socket.join(userRoom(userData.userId)) //Track active logged in user accounts const usersInRoom = io.sockets.adapter.rooms[userRoom(userData.userId)] io.to(userRoom(userData.userId)).emit('update_active_user_count', usersInRoom.length) }).catch(error => { //Don't add user to room if they are not logged in // console.log(error) }) }) socket.on('get_active_user_count', token => { Auth.decodeToken(token) .then(userData => { socket.join(userRoom(userData.userId)) //Track active logged in user accounts const usersInRoom = io.sockets.adapter.rooms[userRoom(userData.userId)] io.to(userRoom(userData.userId)).emit('update_active_user_count', usersInRoom.length) }).catch(error => { // console.log(error) }) }) //Renew Session tokens when users request a new one socket.on('renew_session_token', token => { //Decode the token they currently have Auth.decodeToken(token) .then(userData => { if(userData.active == 1){ //Create a new one using credentials and session keys from current Auth.createToken(userData.userId, userData.masterKey, userData.sessionId, userData.created) .then(newToken => { //Emit new token only to user on socket socket.emit('recievend_new_token', newToken) }) } else { //Attempting to reactivate disabled session, kills it all Auth.terminateSession(userData.sessionId) } }) }) // // The client asks for a NOTE, not a room. The server authorises the request // against the session and derives the room itself, so a client can no longer // name an arbitrary room and be handed its buffered diffs. // socket.on('join_room', ({ noteId, token } = {}) => { Auth.decodeToken(token) .then(userData => { return Note.getRawTextIdForUser(noteId, userData.userId) .then(rawTextId => { // No claim on this note -> no room, no diffs if(rawTextId == null){ return } const room = noteRoom(rawTextId) // Remember what this socket is allowed into, so leave_room and // note_diff never have to trust a client-supplied room again socket.authorizedRooms = socket.authorizedRooms || {} socket.authorizedRooms[noteId] = { room, rawTextId } socket.join(room) //If there are past diffs for this note, send them to the user if(noteDiffs[rawTextId] != undefined){ //Sort all note diffs by when they were created. noteDiffs[rawTextId].sort((a,b) => { return a.time - b.time }) //Emit all sorted diffs to user socket.emit('past_diffs', noteDiffs[rawTextId]) } const usersInRoom = io.sockets.adapter.rooms[room] if(usersInRoom){ //Update users in room count io.to(room).emit('update_user_count', usersInRoom.length) } }) }) .catch(() => { //Not logged in, or no access. Join nothing. }) }) socket.on('leave_room', ({ noteId } = {}) => { // Only rooms this socket was actually authorised into const entry = socket.authorizedRooms && socket.authorizedRooms[noteId] if(!entry){ return } socket.leave(entry.room) delete socket.authorizedRooms[noteId] const usersInRoom = io.sockets.adapter.rooms[entry.room] if(usersInRoom){ io.to(entry.room).emit('update_user_count', usersInRoom.length) } }) socket.on('note_diff', (data = {}) => { // Room comes from what this socket was authorised into, never from the // payload const entry = socket.authorizedRooms && socket.authorizedRooms[data.noteId] if(!entry){ return } const room = entry.room const rawTextId = entry.rawTextId //Log each diff for note if(noteDiffs[rawTextId] == undefined){ noteDiffs[rawTextId] = [] } // Carry the raw-text id through. This used to `delete data.id`, stripping // the only identifier before storing and broadcasting -- so a client with // several notes open on one socket could not tell which note a patch // belonged to, and applied every one to whichever editor it found first. const payload = Object.assign({}, data, { rawTextId, time: +new Date }) delete payload.noteId noteDiffs[rawTextId].push(payload) // Everyone in the note room except the sender socket.to(room).emit('incoming_diff', payload) }) socket.on('truncate_diffs_at_save', (checkpoint = {}) => { const entry = socket.authorizedRooms && socket.authorizedRooms[checkpoint.noteId] if(!entry){ return } checkpoint.rawTextId = entry.rawTextId // NOTE: the comparison below is `diffSet[i].hash == checkpoint`, comparing // a hash string against the whole checkpoint object, so it never matches // and sliceTo stays 0 -- meaning the entire diff set is discarded at every // save. That happens to be the fail-safe outcome (the saved text becomes // the new baseline), and the correct slice direction for a real fix is // ambiguous, so it is deliberately left alone rather than guessed at. let diffSet = noteDiffs[checkpoint.rawTextId] if(diffSet && diffSet.length > 0){ //Make sure all diffs are sorted before cleaning noteDiffs[checkpoint.rawTextId].sort((a,b) => { return a.time - b.time }) // Remove all diffs until it reaches the current hash let sliceTo = 0 for (var i = 0; i < diffSet.length; i++) { if(diffSet[i].hash == checkpoint){ sliceTo = i break } } noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo) if(noteDiffs[checkpoint.rawTextId].length == 0){ delete noteDiffs[checkpoint.rawTextId] } //Debugging else { console.log('Diffset after save') console.log(noteDiffs[checkpoint.rawTextId]) } } }) socket.on('disconnect', function(socket){ // console.log('user disconnected'); }); }); http.listen(ports.socketIo, function(){ console.log(`Socke.io: Listening on port ${ports.socketIo}`) }); //Enable json body parsing in requests. Allows me to post data in ajax calls app.use(express.json({limit: '5mb'})) //App Auth, all requests will come in with a token, decode the token and set global var app.use(function(req, res, next){ //Always null out master key, never allow it set from outside req.headers.userId = null req.headers.masterKey = null req.headers.sessionId = null //auth token set by axios in headers let token = req.headers.authorizationtoken if(token !== undefined && token.length > 0){ Auth.decodeToken(token, req) .then(userData => { //Update headers for the rest of the application req.headers.userId = userData.userId req.headers.masterKey = userData.masterKey req.headers.sessionId = userData.sessionId //Tell front end remaining uses on current token res.set('remainingUses', userData.remainingUses) next() }) .catch(error => { next('Unauthorized') }) } else { next() //No token. Move along. } }) // Test Area // const printResults = true // let UserTest = require('@models/User') // let NoteTest = require('@models/Note') // let AuthTest = require('@helpers/Auth') // Auth.test() // UserTest.keyPairTest('genMan30', '1', printResults) // .then( ({testUserId, masterKey}) => // NoteTest.test(testUserId, masterKey, printResults)) // .then( message => { // if(printResults) console.log(message) // Auth.testTwoFactor() // }) // .catch((error) => { // console.log(error) // }) //Test app.get('/api', (req, res) => res.send('Solidscribe /API is up and running')) //Serve up uploaded files app.use('/api/static', express.static( __dirname+'/../staticFiles' )) //Public routes var public = require('@routes/publicController') app.use('/api/public', public) //user endpoint var user = require('@routes/userController') app.use('/api/user', user) //notes endpoint var notes = require('@routes/noteController') app.use('/api/note', notes) //tags endpoint var tags = require('@routes/tagController') app.use('/api/tag', tags) //notes endpoint var attachment = require('@routes/attachmentController') app.use('/api/attachment', attachment) //quick notes endpoint var quickNote = require('@routes/quicknoteController') app.use('/api/quick-note', quickNote) //cycle tracking endpoint var metricTracking = require('@routes/metrictrackingController') app.use('/api/metric-tracking', metricTracking) //Output running status app.listen(ports.express, () => { console.log(`Express: Listening on port ${ports.express}!`) }) // //Error handlers // //Default error handler just say unauthorized for everything app.use(function (err, req, res, next) { if (err) { res.status(401).send('Unauthorized') return } next() })