feat: added ws online counter

This commit is contained in:
Sergey Yarkov 2022-01-30 01:40:18 +03:00
parent aa74a7527f
commit 7c678797d7
17 changed files with 499 additions and 12 deletions

View File

@ -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<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
constructor(socket: Socket) {
this.socket = socket;
this.onConnected();
this.onDisconnected();
}
/**
* On socket connected
*/
public async onConnected() {}
/**
* On socket disconnected
*/
public async onDisconnected() {}
}

View File

@ -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;
}
}

View File

@ -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,
};
}
}

View File

@ -31,8 +31,7 @@ export default class AuthMiddleware {
* driver * driver
*/ */
let guardLastAttempted: string | undefined; let guardLastAttempted: string | undefined;
const authPromises: Promise<boolean>[] = guards.map(guard => auth.use(guard).check()); const authResults: boolean[] = await Promise.all(guards.map(guard => auth.use(guard).check()));
const authResults: boolean[] = await Promise.all(authPromises);
for (let i = 0; i < authResults.length; i += 1) { for (let i = 0; i < authResults.length; i += 1) {
/** /**
@ -54,7 +53,7 @@ export default class AuthMiddleware {
public async handle(ctx: HttpContextContract, next: () => Promise<void>, customGuards: (keyof GuardsList)[]) { public async handle(ctx: HttpContextContract, next: () => Promise<void>, customGuards: (keyof GuardsList)[]) {
/** /**
* Verify token from cookie and set the authorization header * Set the authorization header for token
*/ */
const token = ctx.request.cookie('token'); const token = ctx.request.cookie('token');
if (token) { if (token) {

81
app/Middleware/Ws/Auth.ts Normal file
View File

@ -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<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>,
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();
}
}

View File

@ -1,7 +1,6 @@
import { inject, Ioc } from '@adonisjs/core/build/standalone'; import { inject, Ioc } from '@adonisjs/core/build/standalone';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import Hash from '@ioc:Adonis/Core/Hash'; import Hash from '@ioc:Adonis/Core/Hash';
import Logger from '@ioc:Adonis/Core/Logger';
/** /**
* Datatypes * Datatypes
@ -68,17 +67,19 @@ export default class AuthService {
const token = await ctx.auth.use(this.authGuard).generate(user, { const token = await ctx.auth.use(this.authGuard).generate(user, {
expiresIn: '1d', expiresIn: '1d',
userRoles: user.roles.map(role => role.slug), userRoles: user.roles.map(role => role.slug),
userName: user.fullname,
}); });
ctx.response.cookie('token', token.token); ctx.response.cookie('token', token.token);
Logger.info(`A token for user with id: "${token.user.id}" was successfully generated.`);
return { return {
success: true, success: true,
status: HttpStatusEnum.OK, status: HttpStatusEnum.OK,
message: 'Login success.', message: 'Login success.',
data: token.toJSON(), data: {
userId: user.id,
...token.toJSON(),
},
}; };
} }

View File

@ -2,6 +2,7 @@ import { inject, Ioc } from '@adonisjs/core/build/standalone';
import IResponse from 'App/Datatypes/Interfaces/IResponse'; import IResponse from 'App/Datatypes/Interfaces/IResponse';
import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum'; import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
import Database from '@ioc:Adonis/Lucid/Database'; import Database from '@ioc:Adonis/Lucid/Database';
import Ws from './WsService';
@inject() @inject()
export default class StatService { export default class StatService {
@ -19,12 +20,15 @@ export default class StatService {
` `
); );
const online = await Ws.sessionStore.getOnlineSessionsCount();
return { return {
success: true, success: true,
status: HttpStatusEnum.OK, status: HttpStatusEnum.OK,
message: 'Fetched stat.', message: 'Fetched stat.',
data: { data: {
...count.rows[0], ...count.rows[0],
online,
}, },
}; };
} }

124
app/Services/WsService.ts Normal file
View File

@ -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<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
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();

90
app/Store/SessionStore.ts Normal file
View File

@ -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<SessionDataType | undefined> {
const values = await this.redisClient.hmget(id, 'userId', 'userName', 'connected');
return this.transform(values);
}
/**
* Returns the all sessions from store
*/
public async getSessions(): Promise<Array<SessionDataType | undefined>> {
const keys = new Set<string>();
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<void> {
this.redisClient
.multi()
.hmset(id, {
userId: data.userId,
userName: data.userName,
connected: data.connected,
})
.expire(id, SESSION_TTL)
.exec();
}
}
export default RedisSessionStore;

View File

@ -50,7 +50,7 @@ const databaseConfig: DatabaseConfig = {
paths: ['./database/seeders/MainSeeder'], paths: ['./database/seeders/MainSeeder'],
}, },
healthCheck: true, healthCheck: true,
debug: true, debug: false,
}, },
}, },
}; };

View File

@ -43,6 +43,14 @@ const redisConfig: RedisConfig = {
keyPrefix: '', keyPrefix: '',
healthCheck: true, healthCheck: true,
}, },
session: {
host: Env.get('REDIS_HOST'),
port: Env.get('REDIS_PORT'),
password: Env.get('REDIS_PASSWORD', ''),
db: 1,
keyPrefix: 'session-',
healthCheck: true,
},
}, },
}; };

View File

@ -8,5 +8,6 @@
declare module '@ioc:Adonis/Addons/Redis' { declare module '@ioc:Adonis/Addons/Redis' {
interface RedisConnectionsList { interface RedisConnectionsList {
local: RedisConnectionConfig; local: RedisConnectionConfig;
session: RedisConnectionConfig;
} }
} }

2
env.ts
View File

@ -26,7 +26,7 @@ export default Env.rules({
PG_USER: Env.schema.string(), PG_USER: Env.schema.string(),
PG_PASSWORD: Env.schema.string.optional(), PG_PASSWORD: Env.schema.string.optional(),
PG_DB_NAME: Env.schema.string(), 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_HOST: Env.schema.string({ format: 'host' }),
REDIS_PORT: Env.schema.number(), REDIS_PORT: Env.schema.number(),
REDIS_PASSWORD: Env.schema.string.optional(), REDIS_PASSWORD: Env.schema.string.optional(),

103
package-lock.json generated
View File

@ -2420,6 +2420,11 @@
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz",
"integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==" "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": { "@szmarczak/http-timer": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@ -2449,6 +2454,21 @@
"@types/responselike": "*" "@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": { "@types/faker": {
"version": "5.5.9", "version": "5.5.9",
"resolved": "https://registry.npmjs.org/@types/faker/-/faker-5.5.9.tgz", "resolved": "https://registry.npmjs.org/@types/faker/-/faker-5.5.9.tgz",
@ -2561,6 +2581,14 @@
"@types/node": "*" "@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": { "@types/stack-trace": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.29.tgz", "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": { "bcrypt": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz",
@ -3620,8 +3653,7 @@
"component-emitter": { "component-emitter": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
"dev": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
@ -3685,6 +3717,15 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" "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": { "cp-file": {
"version": "9.1.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/cp-file/-/cp-file-9.1.0.tgz", "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-9.1.0.tgz",
@ -4066,6 +4107,31 @@
"once": "^1.4.0" "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": { "enquirer": {
"version": "2.3.6", "version": "2.3.6",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", "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": { "sonic-boom": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.2.3.tgz", "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", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" "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": { "xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -43,6 +43,7 @@
"@adonisjs/redis": "^7.0.9", "@adonisjs/redis": "^7.0.9",
"@adonisjs/repl": "^3.1.7", "@adonisjs/repl": "^3.1.7",
"@adonisjs/view": "^6.1.1", "@adonisjs/view": "^6.1.1",
"@types/socket.io": "^3.0.2",
"cloudinary": "^1.26.3", "cloudinary": "^1.26.3",
"luxon": "^1.28.0", "luxon": "^1.28.0",
"nanoid": "^3.1.25", "nanoid": "^3.1.25",
@ -50,6 +51,7 @@
"phc-bcrypt": "^1.0.7", "phc-bcrypt": "^1.0.7",
"proxy-addr": "^2.0.7", "proxy-addr": "^2.0.7",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"socket.io": "^4.4.1",
"source-map-support": "^0.5.19" "source-map-support": "^0.5.19"
} }
} }

View File

@ -7,7 +7,11 @@ export default class AppProvider {
public async boot() {} public async boot() {}
public async ready() {} public async ready() {
if (this.app.environment === 'web') {
await import('../start/socket');
}
}
public async shutdown() {} public async shutdown() {}
} }

6
start/socket.ts Normal file
View File

@ -0,0 +1,6 @@
import Ws from 'App/Services/WsService';
/**
* Start websocket server
*/
Ws.boot();