363 lines
10 KiB
JavaScript
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()
|
|
}) |