diff --git a/app/Controllers/Ws/OnlineController.ts b/app/Controllers/Ws/OnlineController.ts new file mode 100644 index 0000000..d4a796b --- /dev/null +++ b/app/Controllers/Ws/OnlineController.ts @@ -0,0 +1,24 @@ +import Ws, { ClientToServerEvents, InterServerEvents, ServerToClientEvents, SocketData } from 'App/Services/WsService'; +import { Socket } from 'socket.io'; +import InMemorySessionStore from 'App/Store/SessionStore'; +import Logger from '@ioc:Adonis/Core/Logger'; + +export default class OnlineController { + public socket: Socket; + + constructor(socket: Socket) { + this.socket = socket; + this.onConnected(); + this.onDisconnected(); + } + + /** + * On socket connected + */ + public async onConnected() {} + + /** + * On socket disconnected + */ + public async onDisconnected() {} +} diff --git a/app/Helpers/CookieHelper.ts b/app/Helpers/CookieHelper.ts new file mode 100644 index 0000000..2540ffc --- /dev/null +++ b/app/Helpers/CookieHelper.ts @@ -0,0 +1,14 @@ +export default class CookieHelper { + public static parseCookieString(string: string): { [key: string]: string } { + const values = string + .split(';') + .map(v => v.split('=')) + .reduce((acc, value) => { + const [key, ...rest] = value; + acc[decodeURIComponent(key.trim())] = decodeURIComponent(rest.join('=').trim()); + return acc; + }, {}); + + return values; + } +} diff --git a/app/Helpers/OpaqueTokenHelper.ts b/app/Helpers/OpaqueTokenHelper.ts new file mode 100644 index 0000000..34fb587 --- /dev/null +++ b/app/Helpers/OpaqueTokenHelper.ts @@ -0,0 +1,30 @@ +import crypto from 'crypto'; + +export default class OpaqueTokenHelper { + /** + * Parse token + * + * @param token token + * @returns Hashed token with id + */ + public static parseToken(token: string) { + const parts = token.split('.'); + + if (parts.length !== 2) { + throw new Error('E_INVALID_API_TOKEN'); + } + + const id = Buffer.from(parts[0], 'base64').toString('utf-8'); + + if (!id) { + throw new Error('E_INVALID_API_TOKEN'); + } + + const hashed = crypto.createHash('sha256').update(parts[1]).digest('hex'); + + return { + hashed, + id, + }; + } +} diff --git a/app/Middleware/Auth.ts b/app/Middleware/Auth.ts index 14f7874..b2d027b 100644 --- a/app/Middleware/Auth.ts +++ b/app/Middleware/Auth.ts @@ -31,8 +31,7 @@ export default class AuthMiddleware { * driver */ let guardLastAttempted: string | undefined; - const authPromises: Promise[] = guards.map(guard => auth.use(guard).check()); - const authResults: boolean[] = await Promise.all(authPromises); + const authResults: boolean[] = await Promise.all(guards.map(guard => auth.use(guard).check())); for (let i = 0; i < authResults.length; i += 1) { /** @@ -54,7 +53,7 @@ export default class AuthMiddleware { public async handle(ctx: HttpContextContract, next: () => Promise, customGuards: (keyof GuardsList)[]) { /** - * Verify token from cookie and set the authorization header + * Set the authorization header for token */ const token = ctx.request.cookie('token'); if (token) { diff --git a/app/Middleware/Ws/Auth.ts b/app/Middleware/Ws/Auth.ts new file mode 100644 index 0000000..b859d27 --- /dev/null +++ b/app/Middleware/Ws/Auth.ts @@ -0,0 +1,81 @@ +/* eslint-disable no-param-reassign */ +import { nanoid } from 'nanoid'; +import Redis from '@ioc:Adonis/Addons/Redis'; +import CookieHelper from 'App/Helpers/CookieHelper'; +import OpaqueTokenHelper from 'App/Helpers/OpaqueTokenHelper'; +import Ws, { ClientToServerEvents, InterServerEvents, ServerToClientEvents, SocketData } from 'App/Services/WsService'; +import { Socket } from 'socket.io'; +import { ExtendedError } from 'socket.io/dist/namespace'; +import Logger from '@ioc:Adonis/Core/Logger'; + +export default class AuthMiddleware { + /** + * Checks the cookie token in headers and + * validates the opaque token + * + * @param socket + * @param next + * @returns void + */ + public static async authenticate( + socket: Socket, + next: (err?: ExtendedError | undefined) => void + ) { + /** + * Parse and validate token from cookie + */ + const cookie = socket.handshake.headers?.cookie; + + if (!cookie) { + return next(new Error('SocketErrors.MissingParameter')); + } + + const token = CookieHelper.parseCookieString(cookie)?.token; + + if (!token) { + return next(new Error('SocketErrors.MissingParameter')); + } + + const tokenData = JSON.parse(Buffer.from(token.split('.')[0].split(':')[1], 'base64').toString('utf-8')); + const parsed = OpaqueTokenHelper.parseToken(tokenData.message); + const data = await Redis.get(`api:${parsed.id}`); + + if (!data) { + return next(new Error('SocketErrors.BadCredentials')); + } + + /** + * Assign existing session to socket data + */ + const sessionId = socket.handshake.auth.sessionId as string | undefined; + + if (sessionId) { + const session = await Ws.sessionStore.findSession(sessionId); + + if (session) { + Logger.info(`[Ws]: Attacing session from redis session store.`); + + socket.data.sessionId = sessionId; + socket.data.userId = session.userId; + socket.data.userName = session.userName; + + return next(); + } + } + + /** + * Parse data from redis string and create new session + */ + Logger.info(`[Ws]: Attaching token data from cookie.`); + const parsedData = JSON.parse(data); + + /** + * Set custom data to socket + */ + socket.data.sessionId = nanoid(); + socket.data.userId = parsedData.user_id as string; + socket.data.userName = parsedData.userName as string; + + return next(); + } +} diff --git a/app/Services/AuthService.ts b/app/Services/AuthService.ts index 226533f..6b6e47d 100644 --- a/app/Services/AuthService.ts +++ b/app/Services/AuthService.ts @@ -1,7 +1,6 @@ import { inject, Ioc } from '@adonisjs/core/build/standalone'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import Hash from '@ioc:Adonis/Core/Hash'; -import Logger from '@ioc:Adonis/Core/Logger'; /** * Datatypes @@ -68,17 +67,19 @@ export default class AuthService { const token = await ctx.auth.use(this.authGuard).generate(user, { expiresIn: '1d', userRoles: user.roles.map(role => role.slug), + userName: user.fullname, }); ctx.response.cookie('token', token.token); - Logger.info(`A token for user with id: "${token.user.id}" was successfully generated.`); - return { success: true, status: HttpStatusEnum.OK, message: 'Login success.', - data: token.toJSON(), + data: { + userId: user.id, + ...token.toJSON(), + }, }; } diff --git a/app/Services/StatService.ts b/app/Services/StatService.ts index 2dff00b..d50ff32 100644 --- a/app/Services/StatService.ts +++ b/app/Services/StatService.ts @@ -2,6 +2,7 @@ import { inject, Ioc } from '@adonisjs/core/build/standalone'; import IResponse from 'App/Datatypes/Interfaces/IResponse'; import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum'; import Database from '@ioc:Adonis/Lucid/Database'; +import Ws from './WsService'; @inject() export default class StatService { @@ -19,12 +20,15 @@ export default class StatService { ` ); + const online = await Ws.sessionStore.getOnlineSessionsCount(); + return { success: true, status: HttpStatusEnum.OK, message: 'Fetched stat.', data: { ...count.rows[0], + online, }, }; } diff --git a/app/Services/WsService.ts b/app/Services/WsService.ts new file mode 100644 index 0000000..ed332a9 --- /dev/null +++ b/app/Services/WsService.ts @@ -0,0 +1,124 @@ +import Redis from '@ioc:Adonis/Addons/Redis'; +import AdonisServer from '@ioc:Adonis/Core/Server'; +import { Server } from 'socket.io'; + +/** + * Controllers + */ +import OnlineController from 'App/Controllers/Ws/OnlineController'; + +/** + * Middlewares + */ +import AuthMiddleware from 'App/Middleware/Ws/Auth'; +import RedisSessionStore from 'App/Store/SessionStore'; + +export interface ServerToClientEvents { + /** + * User + */ + 'user:session': (data: { sessionId: string | undefined; userId: string | undefined }) => void; + 'user:connected': (data: { userId: string; socketId: string }) => void; + 'user:online': (data: number) => void; +} + +export interface ClientToServerEvents { + /** + * User + */ + 'user:logout': (data: { sessionId: string }) => void; +} + +export interface InterServerEvents {} + +export interface SocketData { + sessionId?: string | undefined; + userId?: string | undefined; + userName?: string | undefined; +} + +class WsService { + public io: Server; + + public sessionStore: RedisSessionStore; + + public booted = false; + + public boot() { + if (this.booted) { + return; + } + + this.booted = true; + this.io = new Server(AdonisServer.instance!, { + cors: { + origin: 'http://localhost:3000', + credentials: true, + }, + }); + this.sessionStore = new RedisSessionStore(Redis.connection('session')); + + this.setupMiddlewares(); + this.listen(); + } + + private listen() { + this.io.on('connection', socket => { + const onlineController = new OnlineController(socket); + + /** + * Set user session and send online count + */ + this.sessionStore + .saveSession(socket.data.sessionId, { + sessionId: socket.data.sessionId, + userId: socket.data.userId, + userName: socket.data.userName, + connected: true, + }) + .then(async () => { + const online = await this.sessionStore.getOnlineSessionsCount(); + socket.emit('user:online', online); + socket.broadcast.emit('user:online', online); + }); + + /** + * Send session to client + */ + socket.emit('user:session', { sessionId: socket.data.sessionId, userId: socket.data.userId }); + + /** + * Broadcast to all clients about new connection + */ + socket.broadcast.emit('user:connected', { + userId: socket.handshake.auth.userId, + socketId: socket.id, + }); + + socket.on('disconnect', async () => { + const matchingSockets = await this.io.in(socket.data.userId).allSockets(); + const isDisconnected = matchingSockets.size === 0; + + if (isDisconnected) { + /** + * Update connection flag of socket to flase + */ + this.sessionStore.saveSession(socket.data.sessionId, { + sessionId: socket.data.sessionId, + userId: socket.data.userId, + userName: socket.data.userName, + connected: false, + }); + + this.sessionStore.getOnlineSessionsCount().then(online => socket.broadcast.emit('user:online', online)); + } + }); + }); + } + + private setupMiddlewares() { + this.io.use(AuthMiddleware.authenticate); + } +} + +export default new WsService(); diff --git a/app/Store/SessionStore.ts b/app/Store/SessionStore.ts new file mode 100644 index 0000000..3b5886c --- /dev/null +++ b/app/Store/SessionStore.ts @@ -0,0 +1,90 @@ +/* eslint-disable max-classes-per-file */ +import { RedisConnectionContract } from '@ioc:Adonis/Addons/Redis'; + +type SessionDataType = { userId: string; userName: string; connected: boolean }; + +const SESSION_TTL = 24 * 60 * 60; // 1 day + +class RedisSessionStore { + public redisClient: RedisConnectionContract; + + constructor(redisClient: RedisConnectionContract) { + this.redisClient = redisClient; + } + + // eslint-disable-next-line class-methods-use-this + private transform(values: (string | null)[]) { + const [userId, userName, connected] = values; + + if (!userId || !userName || !connected) return undefined; + + return { + userId, + userName, + connected: connected === 'true', + }; + } + + /** + * Return session data by session id + * + * @param id Session id + * @returns Session data + */ + public async findSession(id: string): Promise { + const values = await this.redisClient.hmget(id, 'userId', 'userName', 'connected'); + return this.transform(values); + } + + /** + * Returns the all sessions from store + */ + public async getSessions(): Promise> { + const keys = new Set(); + let nextIndex = 0; + + do { + // eslint-disable-next-line no-await-in-loop + const [nextIndexAsStr, result] = await this.redisClient.scan(nextIndex, 'MATCH', '*', 'COUNT', 100); + nextIndex = parseInt(nextIndexAsStr, 10); + result.forEach(s => keys.add(s)); + } while (nextIndex !== 0); + + const commands = Array.from(keys).map(key => ['hmget', key.split('-')[1], 'userId', 'userName', 'connected']); + + return this.redisClient + .multi(commands) + .exec() + .then(results => results.map(([err, session]) => (err ? undefined : this.transform(session)))); + } + + /** + * Returns the unique sessions with connected flag `true` + */ + public async getOnlineSessionsCount() { + const sessions = await this.getSessions(); + + const online = new Set(sessions.filter(s => s?.connected === true).map(s => s?.userId)); + return online.size; + } + + /** + * Set session data to redis store + * + * @param id Session id + * @param data Session data + */ + public async saveSession(id: string, data: SessionDataType): Promise { + this.redisClient + .multi() + .hmset(id, { + userId: data.userId, + userName: data.userName, + connected: data.connected, + }) + .expire(id, SESSION_TTL) + .exec(); + } +} + +export default RedisSessionStore; diff --git a/config/database.ts b/config/database.ts index 6ca9598..56d9e86 100644 --- a/config/database.ts +++ b/config/database.ts @@ -50,7 +50,7 @@ const databaseConfig: DatabaseConfig = { paths: ['./database/seeders/MainSeeder'], }, healthCheck: true, - debug: true, + debug: false, }, }, }; diff --git a/config/redis.ts b/config/redis.ts index 80e0bad..a19f00e 100644 --- a/config/redis.ts +++ b/config/redis.ts @@ -43,6 +43,14 @@ const redisConfig: RedisConfig = { keyPrefix: '', healthCheck: true, }, + session: { + host: Env.get('REDIS_HOST'), + port: Env.get('REDIS_PORT'), + password: Env.get('REDIS_PASSWORD', ''), + db: 1, + keyPrefix: 'session-', + healthCheck: true, + }, }, }; diff --git a/contracts/redis.ts b/contracts/redis.ts index 1f2a08c..55f2fec 100644 --- a/contracts/redis.ts +++ b/contracts/redis.ts @@ -8,5 +8,6 @@ declare module '@ioc:Adonis/Addons/Redis' { interface RedisConnectionsList { local: RedisConnectionConfig; + session: RedisConnectionConfig; } } diff --git a/env.ts b/env.ts index 730aa6c..4dd1fe7 100644 --- a/env.ts +++ b/env.ts @@ -26,7 +26,7 @@ export default Env.rules({ PG_USER: Env.schema.string(), PG_PASSWORD: Env.schema.string.optional(), PG_DB_NAME: Env.schema.string(), - REDIS_CONNECTION: Env.schema.enum(['local'] as const), + REDIS_CONNECTION: Env.schema.enum(['local', 'session'] as const), REDIS_HOST: Env.schema.string({ format: 'host' }), REDIS_PORT: Env.schema.number(), REDIS_PASSWORD: Env.schema.string.optional(), diff --git a/package-lock.json b/package-lock.json index 455091f..86a0dc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2420,6 +2420,11 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", "integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==" }, + "@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==" + }, "@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -2449,6 +2454,21 @@ "@types/responselike": "*" } }, + "@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, "@types/faker": { "version": "5.5.9", "resolved": "https://registry.npmjs.org/@types/faker/-/faker-5.5.9.tgz", @@ -2561,6 +2581,14 @@ "@types/node": "*" } }, + "@types/socket.io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz", + "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==", + "requires": { + "socket.io": "*" + } + }, "@types/stack-trace": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.29.tgz", @@ -3199,6 +3227,11 @@ } } }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "bcrypt": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", @@ -3620,8 +3653,7 @@ "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "concat-map": { "version": "0.0.1", @@ -3685,6 +3717,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cp-file": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-9.1.0.tgz", @@ -4066,6 +4107,31 @@ "once": "^1.4.0" } }, + "engine.io": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz", + "integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", + "ws": "~8.2.3" + } + }, + "engine.io-parser": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "requires": { + "@socket.io/base64-arraybuffer": "~1.0.2" + } + }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -7969,6 +8035,34 @@ } } }, + "socket.io": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", + "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + } + }, + "socket.io-adapter": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==" + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + } + }, "sonic-boom": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.2.3.tgz", @@ -8695,6 +8789,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 406c0f9..37bccde 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@adonisjs/redis": "^7.0.9", "@adonisjs/repl": "^3.1.7", "@adonisjs/view": "^6.1.1", + "@types/socket.io": "^3.0.2", "cloudinary": "^1.26.3", "luxon": "^1.28.0", "nanoid": "^3.1.25", @@ -50,6 +51,7 @@ "phc-bcrypt": "^1.0.7", "proxy-addr": "^2.0.7", "reflect-metadata": "^0.1.13", + "socket.io": "^4.4.1", "source-map-support": "^0.5.19" } } diff --git a/providers/AppProvider.ts b/providers/AppProvider.ts index 96558de..a5d170f 100644 --- a/providers/AppProvider.ts +++ b/providers/AppProvider.ts @@ -7,7 +7,11 @@ export default class AppProvider { public async boot() {} - public async ready() {} + public async ready() { + if (this.app.environment === 'web') { + await import('../start/socket'); + } + } public async shutdown() {} } diff --git a/start/socket.ts b/start/socket.ts new file mode 100644 index 0000000..64a2131 --- /dev/null +++ b/start/socket.ts @@ -0,0 +1,6 @@ +import Ws from 'App/Services/WsService'; + +/** + * Start websocket server + */ +Ws.boot();