From ac2117b64b7339eb2baece51c6dbf50d1dc8a730 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Thu, 5 Mar 2026 14:51:45 +0100 Subject: [PATCH] Keys auf SSE umgestellt --- api/src/app.module.ts | 2 + api/src/modules/key/key.controller.ts | 13 +++++- api/src/modules/key/key.module.ts | 3 +- api/src/modules/key/key.service.ts | 41 ++++++++++++++----- .../realtime/sse/sse-ticket.service.ts | 21 ++++++++++ .../realtime/sse/sse.controller.spec.ts | 18 ++++++++ .../modules/realtime/sse/sse.controller.ts | 38 +++++++++++++++++ api/src/modules/realtime/sse/sse.module.ts | 17 ++++++++ .../modules/realtime/sse/sse.service.spec.ts | 18 ++++++++ api/src/modules/realtime/sse/sse.service.ts | 22 ++++++++++ api/src/modules/user/user.service.ts | 4 ++ .../shared/service/system.helper.service.ts | 2 +- client/src/app/shared/api.service.ts | 33 ++++++++++++++- 13 files changed, 217 insertions(+), 15 deletions(-) create mode 100644 api/src/modules/realtime/sse/sse-ticket.service.ts create mode 100644 api/src/modules/realtime/sse/sse.controller.spec.ts create mode 100644 api/src/modules/realtime/sse/sse.controller.ts create mode 100644 api/src/modules/realtime/sse/sse.module.ts create mode 100644 api/src/modules/realtime/sse/sse.service.spec.ts create mode 100644 api/src/modules/realtime/sse/sse.service.ts diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 54036ce..b56e739 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -15,6 +15,7 @@ import { CacheInterceptor, CacheModule } from '@nestjs/cache-manager'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { MailModule } from './modules/mail/mail.module'; import { LogModule } from './modules/log/log.module'; +import { SseModule } from './modules/realtime/sse/sse.module'; @Module({ imports: [ @@ -33,6 +34,7 @@ import { LogModule } from './modules/log/log.module'; SystemModule, MailModule, LogModule, + SseModule ], controllers: [AppController], providers: [ diff --git a/api/src/modules/key/key.controller.ts b/api/src/modules/key/key.controller.ts index 9b65019..88cec0a 100644 --- a/api/src/modules/key/key.controller.ts +++ b/api/src/modules/key/key.controller.ts @@ -7,48 +7,57 @@ import { Post, Put, Req, + Sse, UseGuards, } from '@nestjs/common'; import { KeyService } from './key.service'; import { AuthenticatedRequest } from 'src/model/interface/authenticated-request.interface'; import { AuthGuard } from 'src/core/guards/auth.guard'; import { Key } from 'src/model/entitites'; +import { interval, map, Observable } from 'rxjs'; + -@UseGuards(AuthGuard) @Controller('key') export class KeyController { constructor(private service: KeyService) {} + @UseGuards(AuthGuard) @Get() getKeys(@Req() req: AuthenticatedRequest) { return this.service.getUsersKeys(req.user); } + @UseGuards(AuthGuard) @Get('lost') getLostKeys(@Req() req: AuthenticatedRequest) { return this.service.getLostKeys(req.user); } + @UseGuards(AuthGuard) @Post() postKey(@Req() req: AuthenticatedRequest, @Body() key: Key) { return this.service.createKey(req.user, key); } + @UseGuards(AuthGuard) @Put() updateKey(@Req() req: AuthenticatedRequest, @Body() key: Key) { return this.service.updateKey(req.user, key); } + @UseGuards(AuthGuard) @Put(':id/restore') restoreKey(@Req() req: AuthenticatedRequest, @Param('id') id: string) { return this.service.restoreKey(req.user, id); } + @UseGuards(AuthGuard) @Delete(':id') deleteKey(@Req() req: AuthenticatedRequest, @Param('id') id: string) { return this.service.deleteKey(req.user, id); } + @UseGuards(AuthGuard) @Post(':id/handover') handoutKey( @Req() req: AuthenticatedRequest, @@ -58,11 +67,13 @@ export class KeyController { return this.service.handoverKey(req.user, body, id); } + @UseGuards(AuthGuard) @Get(':id/handover') getKeyHandouts(@Req() req: AuthenticatedRequest, @Param('id') id: string) { return this.service.getKeyHandovers(req.user, id); } + @UseGuards(AuthGuard) @Get('archive') getArchive(@Req() req: AuthenticatedRequest) { return this.service.getDeletedKeys(req.user); diff --git a/api/src/modules/key/key.module.ts b/api/src/modules/key/key.module.ts index 9c6eb3d..14e7a44 100644 --- a/api/src/modules/key/key.module.ts +++ b/api/src/modules/key/key.module.ts @@ -6,10 +6,11 @@ import { AuthModule } from '../auth/auth.module'; import { SharedServiceModule } from 'src/shared/service/shared.service.module'; import { ConfigService } from '@nestjs/config'; import { MailModule } from '../mail/mail.module'; +import { SseModule } from '../realtime/sse/sse.module'; @Module({ controllers: [KeyController], providers: [KeyService, ConfigService], - imports: [DatabaseModule, AuthModule, SharedServiceModule, MailModule], + imports: [DatabaseModule, AuthModule, SharedServiceModule, MailModule, SseModule], }) export class KeyModule {} diff --git a/api/src/modules/key/key.service.ts b/api/src/modules/key/key.service.ts index c987ed5..b8e872d 100644 --- a/api/src/modules/key/key.service.ts +++ b/api/src/modules/key/key.service.ts @@ -12,6 +12,7 @@ import { FindOperator, IsNull, Not } from 'typeorm'; import { faker } from '@faker-js/faker'; import { ConfigService } from '@nestjs/config'; import { MailService } from '../mail/mail.service'; +import { SseService } from '../realtime/sse/sse.service'; @Injectable() export class KeyService { @@ -23,7 +24,10 @@ export class KeyService { private readonly helper: HelperService, private readonly configService: ConfigService, private readonly mailService: MailService, - ) {} + private readonly sseService: SseService + ) { + console.log("INIT KEYSERVICE") + } get isDevelopMode(): boolean { return (this.configService.get('DEVELOP_MODE') || '').toLowerCase() == 'true'; @@ -87,7 +91,19 @@ export class KeyService { console.error(e); } } - return this.keyrepository.save(this.keyrepository.create(key)); + const saved = await this.keyrepository.save(this.keyrepository.create(key)); + + this.sendKeysToSSE(saved); + return saved; + + } + + private async sendKeysToSSE(key: Key) { + const system = await this.helper.getSystemOfKey(key) + for (let manager of system.managers) { + const keys = await this.getUsersKeys(manager); + this.sseService.sendKeysToUsers(manager.id, keys) + } } @@ -129,7 +145,6 @@ export class KeyService { where: { id: keyID }, relations: [ 'cylinder', 'cylinder.system', 'cylinder.system.managers', 'cylinder.system.managers.settings' ] }); - console.log(managerOb.cylinder[0].system.managers) managerOb.cylinder[0].system.managers.filter(m => m.settings.sendSystemUpdateMails).forEach(m => { this.mailService.sendKeyHandoutMail({ to: m, key, handoutAction: res }) }) @@ -137,7 +152,7 @@ export class KeyService { } catch (e){ console.log(e) } - + this.sendKeysToSSE(key); return res; } @@ -173,18 +188,22 @@ export class KeyService { } } - async createKey(user: User, key: any) { - const k = await this.keyrepository.save(this.keyrepository.create(key)); + async createKey(user: User, key: any): Promise { + const k = await this.keyrepository.save(this.keyrepository.create(key)) as any as Key; this.activityService.logKeyCreated(user, key, key.cylinder[0].system); + this.sendKeysToSSE(k as any) return k; } - async deleteKey(user: User, id: string) { + async deleteKey(user: User, id: string): Promise { const key = await this.keyrepository.findOneOrFail({ where: { id, cylinder: { system: { managers: { id: user.id } } } }, }); await this.activityService.logDeleteKey(user, key); - return this.keyrepository.softRemove(key); + const k = await this.keyrepository.softRemove(key); + this.sendKeysToSSE(k) + return k; + } getDeletedKeys(user: User) { @@ -198,7 +217,7 @@ export class KeyService { }); } - async restoreKey(user: User, keyID: string) { + async restoreKey(user: User, keyID: string): Promise { const key = await this.keyrepository.findOneOrFail({ where: { cylinder: { system: { managers: { id: user.id } } }, id: keyID }, @@ -207,6 +226,8 @@ export class KeyService { key.deletedAt = null; await this.activityService.logKeyRestored(user, key); await this.helper.deleteKeyArchiveCache(); - return this.keyrepository.save(key); + const k = await this.keyrepository.save(key); + this.sendKeysToSSE(k) + return k; } } diff --git a/api/src/modules/realtime/sse/sse-ticket.service.ts b/api/src/modules/realtime/sse/sse-ticket.service.ts new file mode 100644 index 0000000..d17faa9 --- /dev/null +++ b/api/src/modules/realtime/sse/sse-ticket.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class SseTicketService { + private userTickets: Map = new Map(); + + generateTicket(userId: string): {ticket: string } { + const ticket = crypto.randomUUID(); + + this.userTickets.set(ticket, { userId, used: false }); + + return {ticket}; + } + + getUserIdToTicket(ticketId: string): string { + if (!this.userTickets.has(ticketId)) { return null; } + const ticket = this.userTickets.get(ticketId); + if (!ticket || ticket.used) { return null; } + return ticket.userId; + } +} diff --git a/api/src/modules/realtime/sse/sse.controller.spec.ts b/api/src/modules/realtime/sse/sse.controller.spec.ts new file mode 100644 index 0000000..d10cbac --- /dev/null +++ b/api/src/modules/realtime/sse/sse.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SseController } from './sse.controller'; + +describe('SseController', () => { + let controller: SseController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SseController], + }).compile(); + + controller = module.get(SseController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/api/src/modules/realtime/sse/sse.controller.ts b/api/src/modules/realtime/sse/sse.controller.ts new file mode 100644 index 0000000..3b202e6 --- /dev/null +++ b/api/src/modules/realtime/sse/sse.controller.ts @@ -0,0 +1,38 @@ +import { Controller, Get, Param, Query, Req, Sse, UnauthorizedException, UseGuards } from '@nestjs/common'; +import { AuthenticatedRequest } from 'src/model/interface/authenticated-request.interface'; +import { SseTicketService } from './sse-ticket.service'; +import { AuthGuard } from 'src/core/guards/auth.guard'; +import { Observable, interval, map } from 'rxjs'; +import { KeyService } from 'src/modules/key/key.service'; +import { AuthService } from 'src/modules/auth/auth.service'; +import { User } from 'src/model/entitites'; +import { UserService } from 'src/modules/user/user.service'; +import { SseService } from './sse.service'; + +@Controller('sse') +export class SseController { + + constructor(private ticketService: SseTicketService, private sseService: SseService, private userService: UserService) {} + + @UseGuards(AuthGuard) + @Get('ticket') + getTicket(@Req() req: AuthenticatedRequest) { + return this.ticketService.generateTicket(req.user.id) + } + + @Sse('key') + async sse(@Query('ticket') ticket: string): Promise> { + const userId = this.ticketService.getUserIdToTicket(ticket); + if (!userId) throw new UnauthorizedException('Invalid/expired ticket'); + const user = await this.getUserToId(userId); + if (!userId) throw new UnauthorizedException('Invalid/expired ticket'); + + + return this.sseService.register(userId); + } + + private async getUserToId(userId: string): Promise { + const user = await this.userService.getUserById(userId); + return Promise.resolve(user); + } +} diff --git a/api/src/modules/realtime/sse/sse.module.ts b/api/src/modules/realtime/sse/sse.module.ts new file mode 100644 index 0000000..7b5e179 --- /dev/null +++ b/api/src/modules/realtime/sse/sse.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { SseController } from './sse.controller'; +import { DatabaseModule } from 'src/shared/database/database.module'; +import { SseTicketService } from './sse-ticket.service'; +import { AuthModule } from 'src/modules/auth/auth.module'; +import { SharedServiceModule } from 'src/shared/service/shared.service.module'; +import { MailModule } from 'src/modules/mail/mail.module'; +import { UserService } from 'src/modules/user/user.service'; +import { SseService } from './sse.service'; + +@Module({ + controllers: [SseController], + imports: [DatabaseModule, AuthModule, SharedServiceModule, MailModule], + providers: [SseTicketService, UserService, SseService], + exports: [SseService] +}) +export class SseModule {} diff --git a/api/src/modules/realtime/sse/sse.service.spec.ts b/api/src/modules/realtime/sse/sse.service.spec.ts new file mode 100644 index 0000000..1f6f7ea --- /dev/null +++ b/api/src/modules/realtime/sse/sse.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SseService } from './sse.service'; + +describe('SseService', () => { + let service: SseService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SseService], + }).compile(); + + service = module.get(SseService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/api/src/modules/realtime/sse/sse.service.ts b/api/src/modules/realtime/sse/sse.service.ts new file mode 100644 index 0000000..8b38bf9 --- /dev/null +++ b/api/src/modules/realtime/sse/sse.service.ts @@ -0,0 +1,22 @@ +import { Injectable,MessageEvent } from '@nestjs/common'; +import { Subject } from 'rxjs'; +import { Key } from 'src/model/entitites'; + +@Injectable() +export class SseService { + private clients = new Map>(); + + sendKeysToUsers(userId: string, keys: Key[]) { + try { + const sub = this.clients.get(userId); + if (!sub) { return; } + sub.next({ data: keys }) + } catch {} + } + + register(userId: string) { + const subj = new Subject(); + this.clients.set(userId, subj); + return subj; + } +} diff --git a/api/src/modules/user/user.service.ts b/api/src/modules/user/user.service.ts index 51d240d..5751902 100644 --- a/api/src/modules/user/user.service.ts +++ b/api/src/modules/user/user.service.ts @@ -67,4 +67,8 @@ export class UserService { updateSettings(settings: UserSettings) { return this.userSettingsRepository.save(settings); } + + getUserById(id: string) { + return this.userRepo.findOneBy({ id }) + } } diff --git a/api/src/shared/service/system.helper.service.ts b/api/src/shared/service/system.helper.service.ts index 57cbd00..6ae7cdc 100644 --- a/api/src/shared/service/system.helper.service.ts +++ b/api/src/shared/service/system.helper.service.ts @@ -47,7 +47,7 @@ export class HelperService { async getSystemOfKey(key: Key): Promise { const k = await this.keyRepo.findOne({ where: { id: key.id }, - relations: ['cylinder', 'cylinder.system'], + relations: ['cylinder', 'cylinder.system', 'cylinder.system.managers'], withDeleted: true, }); this.cache() diff --git a/client/src/app/shared/api.service.ts b/client/src/app/shared/api.service.ts index d066e15..1c95ac2 100644 --- a/client/src/app/shared/api.service.ts +++ b/client/src/app/shared/api.service.ts @@ -25,7 +25,11 @@ export class ApiService { public settings: BehaviorSubject = new BehaviorSubject(null!); - constructor() { } + constructor() { + setTimeout(() => { + this.setupKeyFeed(); + }, 2000) + } getMe(): Promise { return new Promise(resolve => { @@ -87,7 +91,7 @@ export class ApiService { ).subscribe({ next: (key: IKey) => resolve(key), error: () => resolve(null), - complete: () => { this.refreshKeys(); } + // complete: () => { this.refreshKeys(); } }) }) } @@ -424,4 +428,29 @@ export class ApiService { }) } + + private async setupKeyFeed() { + const ticket = await this.getSSETicket(); + + console.log(ticket) + const sseSource = new EventSource('api/sse/key?ticket=' + ticket); + sseSource.addEventListener('message', (e: MessageEvent) => { + console.log(e.data) + this.keys.next(JSON.parse(e.data)) + }) + sseSource.addEventListener('error', (error) => { + console.error(error) + }) + } + + private getSSETicket() { + return new Promise(resolve => { + this.http.get<{ ticket: string }>('api/sse/ticket').subscribe({ + next: (ticket) => { + return resolve(ticket.ticket) + + } + }) + }) + } }