Files
SolidScribe/server/index.js
T

363 lines
10 KiB
JavaScript

//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()
})