feature: version one notification feature implemented.

This commit is contained in:
2026-07-18 01:09:54 +03:30
parent 0740493384
commit 9f6ec193d2
40 changed files with 1679 additions and 224 deletions

View File

@@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { Server } from 'socket.io';
export function userOrgRoom(userId: string, organizationId: string): string {
return `user:${userId}:org:${organizationId}`;
}
@Injectable()
export class RealtimeEmitter {
private server: Server | null = null;
setServer(server: Server) {
this.server = server;
}
emitToUserOrg(userId: string, organizationId: string, event: string, payload: unknown) {
if (!this.server) return;
this.server.to(userOrgRoom(userId, organizationId)).emit(event, payload);
}
}

View File

@@ -0,0 +1,81 @@
import {
OnGatewayConnection,
OnGatewayInit,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { Server, Socket } from 'socket.io';
import { RealtimeEmitter, userOrgRoom } from './realtime.emitter';
type AccessPayload = {
sub?: string;
organizationId?: string;
type?: string;
};
@WebSocketGateway({
namespace: '/realtime',
cors: {
origin: process.env.FRONTEND_URL || 'http://localhost:3001',
credentials: true,
},
})
export class RealtimeGateway implements OnGatewayInit, OnGatewayConnection {
private readonly logger = new Logger(RealtimeGateway.name);
@WebSocketServer()
server!: Server;
constructor(
private readonly jwt: JwtService,
private readonly config: ConfigService,
private readonly emitter: RealtimeEmitter,
) {}
afterInit(server: Server) {
this.emitter.setServer(server);
}
async handleConnection(client: Socket) {
try {
const token = this.readAccessToken(client);
if (!token) {
client.disconnect(true);
return;
}
const payload = await this.jwt.verifyAsync<AccessPayload>(token, {
secret: this.config.get<string>('jwt.secret'),
});
if (!payload?.sub || !payload.organizationId || payload.type !== 'access') {
client.disconnect(true);
return;
}
const room = userOrgRoom(payload.sub, payload.organizationId);
await client.join(room);
client.data.userId = payload.sub;
client.data.organizationId = payload.organizationId;
} catch (error) {
this.logger.debug(`Realtime auth failed: ${String(error)}`);
client.disconnect(true);
}
}
private readAccessToken(client: Socket): string | null {
const cookieHeader = client.handshake.headers.cookie;
if (!cookieHeader) return null;
const parts = cookieHeader.split(';');
for (const part of parts) {
const [rawKey, ...rest] = part.trim().split('=');
if (rawKey === 'accessToken') {
return decodeURIComponent(rest.join('='));
}
}
return null;
}
}

View File

@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RealtimeEmitter } from './realtime.emitter';
import { RealtimeGateway } from './realtime.gateway';
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get<string>('jwt.secret'),
}),
}),
],
providers: [RealtimeEmitter, RealtimeGateway],
exports: [RealtimeEmitter],
})
export class RealtimeModule {}