import 'regenerator-runtime/runtime' import path from 'path' import dotenv from 'dotenv' dotenv.config({ path: path.join(__dirname, '../.env') }) import http from 'http' import express from 'express' import proxy from 'express-http-proxy' import bodyParser from 'body-parser' import cookieParser from 'cookie-parser' import session from 'express-session' import connectRedis from 'connect-redis' import { Server as SocketIO } from 'socket.io' import { createAdapter } from 'socket.io-redis' import passportSocketIo from 'passport.socketio' import morgan from 'morgan' import logger, { morganStream } from './logger' import passport from 'passport' import mongoose from 'mongoose' import UserModel from './models/user' import redisClient, { asyncHEXISTS, asyncHGET, asyncHSET, asyncHDEL, asyncHGETALL, asyncDEL, } from './redis' import { authRouter, initPassport } from './auth/passport' const RedisStore = connectRedis(session) const mongoHost = process.env.MONGODB_HOST ?? 'localhost' mongoose.connect( `mongodb://${mongoHost}:27017`, { useCreateIndex: true, useNewUrlParser: true, useUnifiedTopology: true, user: process.env.MONGODB_USER, pass: process.env.MONGODB_PASSWORD, dbName: 'museum', }, () => { logger.info('Connection to MongoDB has been established successfully.') }, ) const app = express() app.set('trust proxy', 1) const server = http.createServer(app) asyncDEL('socket') asyncDEL('transform') const io = new SocketIO(server, { serveClient: false }) const pubClient = redisClient const subClient = pubClient.duplicate() io.adapter( createAdapter({ pubClient, subClient, }), ) app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: false })) app.use(cookieParser(process.env.SESSION_SECRET)) app.use( session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, }), ) app.use(passport.initialize()) app.use(passport.session()) initPassport() app.use('/auth', authRouter) app.use(morgan('short', { stream: morganStream })) async function onAuthorizeSuccess(data, accept) { if (data.user._id) { const userExists = await asyncHEXISTS('socket', String(data.user._id)) if (userExists === 1) { logger.debug( `${data.user._id} (${data.user.email}) is already logged in. Kicking...`, ) const socketId = await asyncHGET('socket', String(data.user._id)) const socket = io.of('/').sockets.get(socketId) if (socket) { logger.debug(`Disconnecting ${socketId}`) socket.disconnect() } } logger.debug( `Successful connection to socket.io from ${data.user._id} (${data.user.email})`, ) accept(null, true) } } function onAuthorizeFail(_, message, error, accept) { if (error) throw new Error(message) logger.debug('Client failed to connect to socket.io:', message) accept(null, false) } io.use( passportSocketIo.authorize({ // @ts-ignore cookieParser: cookieParser, secret: process.env.SESSION_SECRET, store: new RedisStore({ client: redisClient }), success: onAuthorizeSuccess, fail: onAuthorizeFail, }), ) io.on('connection', async (socket) => { await asyncHSET('socket', String(socket.request.user._id), String(socket.id)) logger.debug(`Assigned ${socket.id} to ${socket.request.user._id}`) socket.on('transform', async (payload) => { await asyncHSET('transform', String(socket.id), JSON.stringify(payload)) }) socket.on('disconnect', async () => { logger.debug(`User with id ${socket.id} disconnected`) const user = await UserModel.findById(socket.request.user._id) if (user) { const transform = await asyncHGET('transform', String(socket.id)) user.lastLocation = JSON.parse(transform) user.save() } await asyncHDEL('transform', String(socket.id)) await asyncHDEL('socket', String(socket.request.user._id)) }) const user = await UserModel.findById(socket.request.user._id) if (user) { socket.emit('initial-transform', user.lastLocation) } }) async function broadcastTransforms() { const transforms = await asyncHGETALL('transform') if (transforms) { let parsed = {} Object.entries(transforms).forEach(([key, value]) => { parsed = { ...parsed, [key]: { ...JSON.parse(value) } } }) io.sockets.emit('broadcast-transforms', parsed) } setTimeout(broadcastTransforms, 30) } broadcastTransforms() if (process.env.NODE_ENV !== 'production') { app.use('/', proxy('http://localhost:4000/')) } else { const buildPath = path.resolve(__dirname, '../../client/build') const indexHtml = path.join(buildPath, 'index.html') app.use(express.static(buildPath)) app.get('*', (_, res) => res.sendFile(indexHtml)) } const port = process.env.PORT ?? 3000 server.listen(port, () => { logger.info(`Server ready on port ${port}`) })