import * as socketio from "socket.io"; import { Server } from 'http'; import { JWT_SECRET, logger } from "../app"; import { randomString, verifyJWT } from "./crypto"; import { decode, sign } from "jsonwebtoken"; import { User } from "../models/user/user.model"; import { IPhone } from "../models/phone/phone.interface"; import { IUser } from "../models/user/user.interface"; import { Phone } from "../models/phone/phone.model"; /** * This class handles all SocketIO connections. * * *SocketIO is another layer ontop of WebSockets* */ export class SocketManager { io: socketio.Server; /** * Frontends have limited access to socket.io features. They just sit in connection and wait for any events. */ frontends: Array; /** * A phone has some more privileges. They activly send new data and thus have write access. */ phones: Array; constructor(httpServer: Server) { logger.debug("Preparing real-time communication ..."); this.frontends = []; this.phones = []; this.io = new socketio.Server(); this.io.listen(httpServer); this.init(); } getUserRoom(user: IUser) { return `user-${user.id}`; } getUserFrontendRoom(user: IUser) { return `user-${user.id}-frontend`; } getUserPhoneRoom(user: IUser) { return `user-${user.id}-phone`; } init() { this.io.on('connection', socket => { socket.on('requestAccess', async data => { data = JSON.parse(data); let token: string = data.token; let phone: IPhone = data.phone; // If request is faulty or token invalid -> return. if (data === undefined || phone === undefined) return; if (await !verifyJWT(token)) return; const id = decode(token, { json: true })!.user; const user = await User.findById(id); // If user doesn't exist -> return. if (user === null) return; const approvalCode = randomString(6, '0123456789'); // Create phone const newPhone = await Phone.create({ ...phone, user, approval: { code: approvalCode } }); this.io.to(this.getUserRoom(user)).emit('approvePhone', newPhone); // Respond with id so device can later submit correct code. socket.emit('requestAccess', { phoneId: newPhone.id }); logger.info(`User ${user?.name} requests to connect new phone ${phone.displayName}`); }); socket.on('submitPairCode', async data => { const { phoneId, code } = JSON.parse(data); console.log("Entry:", data, phoneId, code); if (phoneId === undefined || code === undefined) return; const phone = await Phone.findById(phoneId); if (phone === null) return; console.log(data, phoneId, code); // If provided code isn't equal with actual code -> Emit event again. if (phone.approval.code !== code) { console.log(data, phoneId, code); socket.emit('submitPairCode', ''); return; } phone.approval.approvedOn = new Date(); await phone.save(); // We're good. Create JWT token. const token = sign({ user: phone.user._id, type: 'phone' }, JWT_SECRET, { expiresIn: '30d' }); socket.emit('submitPairCode', token); }); socket.on('loginFrontend', async (token: string) => { if (await verifyJWT(token)) { const tokenDecoded = decode(token, { json: true }); const id = tokenDecoded!.user; const type = tokenDecoded!.type; const user = await User.findById(id); if (user == null) return; if (type != 'frontend') return; if (this.frontends.indexOf(socket.id) != -1) this.frontends.push(socket.id); socket.join(this.getUserRoom(user)); socket.join(this.getUserFrontendRoom(user)); logger.info(`Socket ${socket.id} became a frontend socket.`); } }); socket.on('loginPhone', async (token: string) => { if (await verifyJWT(token)) { const tokenDecoded = decode(token, { json: true }); const id = tokenDecoded!.user; const type = tokenDecoded!.type; const user = await User.findById(id); if (user == null) return; if (type != 'phone') return; if (this.frontends.indexOf(socket.id) != -1) this.frontends.push(socket.id); socket.join(this.getUserRoom(user)); socket.join(this.getUserPhoneRoom(user)); logger.info(`Socket ${socket.id} became a phone socket.`); } }); logger.info(`New socket connection from ${socket.handshake.address} with id ${socket.id} (total connections: ${this.io.sockets.sockets.size})`); socket.emit('test', 'Yay, it works.'); }); } }