From a0e4b57de72bd91eb571321d6d0196317de95950 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 13 Jul 2025 10:17:33 +0300 Subject: [PATCH] add stream token --- backend/src/core/graphql/schema.gql | 10 ++++ .../inputs/generate-stream-token.input.ts | 15 +++++ .../stream/models/generate-token.model.ts | 7 +++ backend/src/module/stream/stream.resolver.ts | 7 +++ backend/src/module/stream/stream.service.ts | 56 +++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 backend/src/module/stream/inputs/generate-stream-token.input.ts create mode 100644 backend/src/module/stream/models/generate-token.model.ts diff --git a/backend/src/core/graphql/schema.gql b/backend/src/core/graphql/schema.gql index d10e6cb..2ba3118 100644 --- a/backend/src/core/graphql/schema.gql +++ b/backend/src/core/graphql/schema.gql @@ -61,6 +61,15 @@ input FilterInput { take: Float } +input GenerateStreamTokenInput { + channelId: String! + userId: String! +} + +type GenerateTokenModel { + token: String! +} + type LocationModel { city: String! country: String! @@ -88,6 +97,7 @@ type Mutation { deactivateAccount(data: DeactivateAccountInput!): AuthModel! disableTotp: Boolean! enableTotp(data: EnableTotpInput!): Boolean! + generateStreamToken(data: GenerateStreamTokenInput!): GenerateTokenModel! loginUser(data: LoginInput!): AuthModel! logoutUser: Boolean! removeProfileAvatar: Boolean! diff --git a/backend/src/module/stream/inputs/generate-stream-token.input.ts b/backend/src/module/stream/inputs/generate-stream-token.input.ts new file mode 100644 index 0000000..6c05df9 --- /dev/null +++ b/backend/src/module/stream/inputs/generate-stream-token.input.ts @@ -0,0 +1,15 @@ +import { Field, InputType } from '@nestjs/graphql' +import { IsNotEmpty, IsString } from 'class-validator' + +@InputType() +export class GenerateStreamTokenInput { + @Field(() => String) + @IsString() + @IsNotEmpty() + public userId: string + + @Field(() => String) + @IsString() + @IsNotEmpty() + public channelId: string +} diff --git a/backend/src/module/stream/models/generate-token.model.ts b/backend/src/module/stream/models/generate-token.model.ts new file mode 100644 index 0000000..adc829b --- /dev/null +++ b/backend/src/module/stream/models/generate-token.model.ts @@ -0,0 +1,7 @@ +import { Field, ObjectType } from '@nestjs/graphql' + +@ObjectType() +export class GenerateTokenModel { + @Field(() => String) + public token: string +} diff --git a/backend/src/module/stream/stream.resolver.ts b/backend/src/module/stream/stream.resolver.ts index 47abb95..30cd00e 100644 --- a/backend/src/module/stream/stream.resolver.ts +++ b/backend/src/module/stream/stream.resolver.ts @@ -1,5 +1,7 @@ import { ChangeStreamInfoInput } from '@/src/module/stream/inputs/change-stream-info.input' import { FilterInput } from '@/src/module/stream/inputs/filter.input' +import { GenerateStreamTokenInput } from '@/src/module/stream/inputs/generate-stream-token.input' +import { GenerateTokenModel } from '@/src/module/stream/models/generate-token.model' import { StreamModel } from '@/src/module/stream/models/stream.model' import { Authorization } from '@/src/shared/decorators/auth.decorator' import { Authorized } from '@/src/shared/decorators/authorized.decorator' @@ -49,4 +51,9 @@ export class StreamResolver { public async removeAvatar(@Authorized() user: User) { return this.streamService.removeThumbnail(user) } + + @Mutation(() => GenerateTokenModel, { name: 'generateStreamToken' }) + public async generateStreamToken(@Args('data') input: GenerateStreamTokenInput) { + return this.streamService.generateToken(input) + } } diff --git a/backend/src/module/stream/stream.service.ts b/backend/src/module/stream/stream.service.ts index d5dbe08..d690f92 100644 --- a/backend/src/module/stream/stream.service.ts +++ b/backend/src/module/stream/stream.service.ts @@ -3,8 +3,11 @@ import { PrismaService } from '@/src/core/prisma/prisma.service' import { StorageService } from '@/src/module/libs/storage/storage.service' import { ChangeStreamInfoInput } from '@/src/module/stream/inputs/change-stream-info.input' import { FilterInput } from '@/src/module/stream/inputs/filter.input' +import { GenerateStreamTokenInput } from '@/src/module/stream/inputs/generate-stream-token.input' import { Injectable, NotFoundException } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' import * as Upload from 'graphql-upload/Upload' +import { AccessToken } from 'livekit-server-sdk' import sharp from 'sharp' @Injectable() @@ -12,6 +15,7 @@ export class StreamService { constructor( private readonly prismaService: PrismaService, private readonly storageService: StorageService, + private readonly configService: ConfigService, ) { } @@ -150,4 +154,56 @@ export class StreamService { return stream } + + public async generateToken(input: GenerateStreamTokenInput) { + const { channelId, userId } = input + + let self: { id: string, username: string } + + const user = await this.prismaService.user.findUnique({ + where: { id: userId }, + }) + + if (user) { + self = { + id: user.id, + username: user.name, + } + } + else { + self = { + id: userId, + username: `Зритель ${Math.floor(Math.random() * 100000)}`, + } + } + + const channel = await this.prismaService.user.findUnique({ + where: { id: channelId }, + }) + + if (!channel) { + throw new NotFoundException('Канал не найден') + } + + const isHost = self.id === channel.id + + const token = new AccessToken( + this.configService.getOrThrow('LIVEKIT_API_KEY'), + this.configService.getOrThrow('LIVEKIT_API_SECRET'), + { + identity: isHost ? `Host -${self.id}` : self.id.toString(), + name: self.username, + }, + ) + + token.addGrant({ + room: channel.id, + roomJoin: true, + canPublish: false, + }) + + return { + token: token.toJwt(), + } + } }