From a4b743eb58b4c14a55ee733fb9656849d4496106 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 25 Mar 2025 02:29:00 +0300 Subject: [PATCH 1/4] add client entity --- libs/common/src/database/database.module.ts | 2 + .../src/modules/client/dtos/client.dto.ts | 20 +++++++++ libs/common/src/modules/client/dtos/index.ts | 1 + .../modules/client/entities/client.entity.ts | 42 +++++++++++++++++++ .../src/modules/client/entities/index.ts | 1 + .../client/repositories/client.repository.ts | 10 +++++ .../src/modules/client/repositories/index.ts | 1 + 7 files changed, 77 insertions(+) create mode 100644 libs/common/src/modules/client/dtos/client.dto.ts create mode 100644 libs/common/src/modules/client/dtos/index.ts create mode 100644 libs/common/src/modules/client/entities/client.entity.ts create mode 100644 libs/common/src/modules/client/entities/index.ts create mode 100644 libs/common/src/modules/client/repositories/client.repository.ts create mode 100644 libs/common/src/modules/client/repositories/index.ts diff --git a/libs/common/src/database/database.module.ts b/libs/common/src/database/database.module.ts index 59e3f08..a25cf35 100644 --- a/libs/common/src/database/database.module.ts +++ b/libs/common/src/database/database.module.ts @@ -42,6 +42,7 @@ import { SpaceLinkEntity } from '../modules/space/entities/space-link.entity'; import { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity'; import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity'; import { TagEntity } from '../modules/space/entities/tag.entity'; +import { ClientEntity } from '../modules/client/entities'; @Module({ imports: [ TypeOrmModule.forRootAsync({ @@ -93,6 +94,7 @@ import { TagEntity } from '../modules/space/entities/tag.entity'; SubspaceModelProductAllocationEntity, SpaceProductAllocationEntity, SubspaceProductAllocationEntity, + ClientEntity, ], namingStrategy: new SnakeNamingStrategy(), synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), diff --git a/libs/common/src/modules/client/dtos/client.dto.ts b/libs/common/src/modules/client/dtos/client.dto.ts new file mode 100644 index 0000000..fd12bc9 --- /dev/null +++ b/libs/common/src/modules/client/dtos/client.dto.ts @@ -0,0 +1,20 @@ +import { IsArray, IsNotEmpty, IsString } from 'class-validator'; + +export class ClientDto { + @IsString() + @IsNotEmpty() + public uuid: string; + + @IsString() + @IsNotEmpty() + public clientId: string; + @IsString() + @IsNotEmpty() + public clientSecret: string; + @IsString() + @IsNotEmpty() + public redirectUri: string; + @IsArray() + @IsNotEmpty() + public scopes: string[]; +} diff --git a/libs/common/src/modules/client/dtos/index.ts b/libs/common/src/modules/client/dtos/index.ts new file mode 100644 index 0000000..f347f86 --- /dev/null +++ b/libs/common/src/modules/client/dtos/index.ts @@ -0,0 +1 @@ +export * from './client.dto'; diff --git a/libs/common/src/modules/client/entities/client.entity.ts b/libs/common/src/modules/client/entities/client.entity.ts new file mode 100644 index 0000000..81c7a9b --- /dev/null +++ b/libs/common/src/modules/client/entities/client.entity.ts @@ -0,0 +1,42 @@ +import { Entity, Column, Unique } from 'typeorm'; +import { AbstractEntity } from '../../abstract/entities/abstract.entity'; +import { ClientDto } from '../dtos'; + +@Entity({ name: 'clients' }) +@Unique(['clientId']) +export class ClientEntity extends AbstractEntity { + @Column({ + type: 'uuid', + default: () => 'gen_random_uuid()', + nullable: false, + }) + public uuid: string; + + @Column({ + length: 255, + nullable: false, + }) + name: string; + + @Column({ + length: 255, + nullable: false, + unique: true, + }) + clientId: string; + + @Column({ + length: 255, + nullable: false, + }) + clientSecret: string; + + @Column({ + length: 255, + nullable: false, + }) + redirectUri: string; + + @Column('simple-array') + scopes: string[]; +} diff --git a/libs/common/src/modules/client/entities/index.ts b/libs/common/src/modules/client/entities/index.ts new file mode 100644 index 0000000..0f193b5 --- /dev/null +++ b/libs/common/src/modules/client/entities/index.ts @@ -0,0 +1 @@ +export * from './client.entity'; diff --git a/libs/common/src/modules/client/repositories/client.repository.ts b/libs/common/src/modules/client/repositories/client.repository.ts new file mode 100644 index 0000000..3fbeead --- /dev/null +++ b/libs/common/src/modules/client/repositories/client.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { ClientEntity } from '../entities'; + +@Injectable() +export class ClientRepository extends Repository { + constructor(private dataSource: DataSource) { + super(ClientEntity, dataSource.createEntityManager()); + } +} diff --git a/libs/common/src/modules/client/repositories/index.ts b/libs/common/src/modules/client/repositories/index.ts new file mode 100644 index 0000000..9812228 --- /dev/null +++ b/libs/common/src/modules/client/repositories/index.ts @@ -0,0 +1 @@ +export * from './client.repository'; From c5dac6c564241b2b10fdd521fa94e6b0bd48243b Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:10:08 +0300 Subject: [PATCH 2/4] add register client api --- .../client/client.repository.module.ts | 9 ++++ src/app.module.ts | 2 + src/client/client.module.ts | 13 ++++++ src/client/controllers/client.controller.ts | 15 +++++++ src/client/controllers/index.ts | 1 + src/client/dtos/index.ts | 3 ++ src/client/dtos/register-client.dto.ts | 31 +++++++++++++ src/client/services/client.service.ts | 45 +++++++++++++++++++ src/client/services/index.ts | 1 + 9 files changed, 120 insertions(+) create mode 100644 libs/common/src/modules/client/client.repository.module.ts create mode 100644 src/client/client.module.ts create mode 100644 src/client/controllers/client.controller.ts create mode 100644 src/client/controllers/index.ts create mode 100644 src/client/dtos/index.ts create mode 100644 src/client/dtos/register-client.dto.ts create mode 100644 src/client/services/client.service.ts create mode 100644 src/client/services/index.ts diff --git a/libs/common/src/modules/client/client.repository.module.ts b/libs/common/src/modules/client/client.repository.module.ts new file mode 100644 index 0000000..af1f3b0 --- /dev/null +++ b/libs/common/src/modules/client/client.repository.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ClientEntity } from './entities'; + +@Module({ + imports: [TypeOrmModule.forFeature([ClientEntity])], + exports: [TypeOrmModule], +}) +export class ClientRepositoryModule {} diff --git a/src/app.module.ts b/src/app.module.ts index f2a5bd8..3187210 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -29,11 +29,13 @@ import { RoleModule } from './role/role.module'; import { TermsConditionsModule } from './terms-conditions/terms-conditions.module'; import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module'; import { TagModule } from './tags/tags.module'; +import { ClientModule } from './client/client.module'; @Module({ imports: [ ConfigModule.forRoot({ load: config, }), + ClientModule, AuthenticationModule, UserModule, InviteUserModule, diff --git a/src/client/client.module.ts b/src/client/client.module.ts new file mode 100644 index 0000000..84bc096 --- /dev/null +++ b/src/client/client.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { ClientController } from './controllers'; +import { ClientService } from './services'; +import { ClientRepositoryModule } from '@app/common/modules/client/client.repository.module'; +import { ClientRepository } from '@app/common/modules/client/repositories'; + +@Module({ + imports: [ClientRepositoryModule], + controllers: [ClientController], + providers: [ClientService, ClientRepository], + exports: [ClientService], +}) +export class ClientModule {} diff --git a/src/client/controllers/client.controller.ts b/src/client/controllers/client.controller.ts new file mode 100644 index 0000000..edf22ea --- /dev/null +++ b/src/client/controllers/client.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Post, Body } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { RegisterClientDto } from '../dtos/register-client.dto'; +import { ClientService } from '../services'; + +@ApiTags('OAuth Clients') +@Controller('clients') +export class ClientController { + constructor(private readonly clientService: ClientService) {} + + @Post('register') + async registerClient(@Body() dto: RegisterClientDto) { + return this.clientService.registerClient(dto); + } +} diff --git a/src/client/controllers/index.ts b/src/client/controllers/index.ts new file mode 100644 index 0000000..a83bab5 --- /dev/null +++ b/src/client/controllers/index.ts @@ -0,0 +1 @@ +export * from './client.controller'; diff --git a/src/client/dtos/index.ts b/src/client/dtos/index.ts new file mode 100644 index 0000000..34d8bbb --- /dev/null +++ b/src/client/dtos/index.ts @@ -0,0 +1,3 @@ +export * from './add.community.dto'; +export * from './project.param.dto'; +export * from './get.community.dto'; diff --git a/src/client/dtos/register-client.dto.ts b/src/client/dtos/register-client.dto.ts new file mode 100644 index 0000000..bdcc573 --- /dev/null +++ b/src/client/dtos/register-client.dto.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class RegisterClientDto { + @ApiProperty({ + example: 'SmartHomeApp', + description: 'The name of the client', + required: true, + }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ + example: 'https://client-app.com/callback', + description: 'The redirect URI of the client', + required: true, + }) + @IsString() + @IsNotEmpty() + redirectUri: string; + + @ApiProperty({ + example: ['DEVICE_SINGLE_CONTROL', 'DEVICE_VIEW'], + description: 'The scopes of the client', + required: true, + }) + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + scopes: string[]; +} diff --git a/src/client/services/client.service.ts b/src/client/services/client.service.ts new file mode 100644 index 0000000..795e33a --- /dev/null +++ b/src/client/services/client.service.ts @@ -0,0 +1,45 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import * as crypto from 'crypto'; +import { RegisterClientDto } from '../dtos/register-client.dto'; +import { ClientEntity } from '@app/common/modules/client/entities'; + +@Injectable() +export class ClientService { + constructor( + @InjectRepository(ClientEntity) + private clientRepository: Repository, + ) {} + + async registerClient(dto: RegisterClientDto) { + const clientId = crypto.randomBytes(16).toString('hex'); + const clientSecret = crypto.randomBytes(32).toString('hex'); + + const client = this.clientRepository.create({ + name: dto.name, + clientId, + clientSecret, + redirectUri: dto.redirectUri, + scopes: dto.scopes, + }); + + await this.clientRepository.save(client); + + return { clientId, clientSecret }; + } + + async validateClient( + clientId: string, + clientSecret: string, + ): Promise { + const client = await this.clientRepository.findOne({ + where: { clientId, clientSecret }, + }); + if (!client) { + throw new NotFoundException('Invalid client credentials'); + } + return client; + } +} diff --git a/src/client/services/index.ts b/src/client/services/index.ts new file mode 100644 index 0000000..b58570a --- /dev/null +++ b/src/client/services/index.ts @@ -0,0 +1 @@ +export * from './client.service'; From cbd3f5028b5a2049719b9fa3ad6a6fa1d669a683 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Thu, 27 Mar 2025 01:28:40 +0300 Subject: [PATCH 3/4] finished client endpoints --- libs/common/src/auth/services/auth.service.ts | 21 +++++--- libs/common/src/constants/controller-route.ts | 13 +++++ src/auth/auth.module.ts | 14 ++++-- src/auth/controllers/user-auth.controller.ts | 3 ++ src/client/client.module.ts | 13 ++++- src/client/controllers/client.controller.ts | 22 ++++++++- src/client/dtos/index.ts | 4 +- src/client/dtos/token-client.dto.ts | 22 +++++++++ src/client/services/client.service.ts | 29 +++++++++-- src/guards/client.guard.ts | 49 +++++++++++++++++++ 10 files changed, 169 insertions(+), 21 deletions(-) create mode 100644 src/client/dtos/token-client.dto.ts create mode 100644 src/guards/client.guard.ts diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index c424522..41ef028 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -77,21 +77,28 @@ export class AuthService { return await this.sessionRepository.save(data); } - async getTokens(payload) { + async getTokens( + payload, + isRefreshToken = true, + accessTokenExpiry = '24h', + refreshTokenExpiry = '30d', + ) { const [accessToken, refreshToken] = await Promise.all([ this.jwtService.signAsync(payload, { secret: this.configService.get('JWT_SECRET'), - expiresIn: '24h', - }), - this.jwtService.signAsync(payload, { - secret: this.configService.get('JWT_SECRET'), - expiresIn: '7d', + expiresIn: accessTokenExpiry, }), + isRefreshToken + ? this.jwtService.signAsync(payload, { + secret: this.configService.get('JWT_SECRET'), + expiresIn: refreshTokenExpiry, + }) + : null, ]); return { accessToken, - refreshToken, + ...(isRefreshToken ? { refreshToken } : {}), }; } diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 0ab4a1b..29a8010 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -1,4 +1,17 @@ export class ControllerRoute { + static CLIENT = class { + public static readonly ROUTE = 'client'; + + static ACTIONS = class { + public static readonly REGISTER_NEW_CLIENT_SUMMARY = + 'Register a new client'; + public static readonly REGISTER_NEW_CLIENT_DESCRIPTION = + 'This endpoint registers a new client in the system.'; + public static readonly LOGIN_CLIENT_SUMMARY = 'Login a client'; + public static readonly LOGIN_CLIENT_DESCRIPTION = + 'This endpoint allows a client to log in to the system.'; + }; + }; static PROJECT = class { public static readonly ROUTE = 'projects'; static ACTIONS = class { diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 8aaef36..df23572 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,17 +1,18 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { UserRepositoryModule } from '@app/common/modules/user/user.repository.module'; -import { CommonModule } from '../../libs/common/src'; -import { UserAuthController } from './controllers'; -import { UserAuthService } from './services'; import { UserRepository } from '@app/common/modules/user/repositories'; import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository'; import { UserOtpRepository } from '@app/common/modules/user/repositories'; import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; import { RoleService } from 'src/role/services'; +import { UserAuthController } from './controllers'; +import { UserAuthService } from './services'; +import { AuthService } from '@app/common/auth/services/auth.service'; +import { EmailService } from '@app/common/util/email.service'; +import { JwtService } from '@nestjs/jwt'; @Module({ - imports: [ConfigModule, UserRepositoryModule, CommonModule], + imports: [ConfigModule], controllers: [UserAuthController], providers: [ UserAuthService, @@ -20,6 +21,9 @@ import { RoleService } from 'src/role/services'; UserOtpRepository, RoleTypeRepository, RoleService, + AuthService, + EmailService, + JwtService, ], exports: [UserAuthService], }) diff --git a/src/auth/controllers/user-auth.controller.ts b/src/auth/controllers/user-auth.controller.ts index 616f4ad..675c264 100644 --- a/src/auth/controllers/user-auth.controller.ts +++ b/src/auth/controllers/user-auth.controller.ts @@ -19,6 +19,7 @@ import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard'; import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; import { OtpType } from '@app/common/constants/otp-type.enum'; import { ControllerRoute } from '@app/common/constants/controller-route'; +import { ClientGuard } from 'src/guards/client.guard'; @Controller({ version: EnableDisableStatusEnum.ENABLED, @@ -29,6 +30,8 @@ export class UserAuthController { constructor(private readonly userAuthService: UserAuthService) {} @ResponseMessage('User Registered Successfully') + @ApiBearerAuth() + @UseGuards(ClientGuard) @Post('user/signup') @ApiOperation({ summary: ControllerRoute.AUTHENTICATION.ACTIONS.SIGN_UP_SUMMARY, diff --git a/src/client/client.module.ts b/src/client/client.module.ts index 84bc096..084ba73 100644 --- a/src/client/client.module.ts +++ b/src/client/client.module.ts @@ -3,11 +3,22 @@ import { ClientController } from './controllers'; import { ClientService } from './services'; import { ClientRepositoryModule } from '@app/common/modules/client/client.repository.module'; import { ClientRepository } from '@app/common/modules/client/repositories'; +import { AuthService } from '@app/common/auth/services/auth.service'; +import { JwtService } from '@nestjs/jwt'; +import { UserRepository } from '@app/common/modules/user/repositories'; +import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository'; @Module({ imports: [ClientRepositoryModule], controllers: [ClientController], - providers: [ClientService, ClientRepository], + providers: [ + ClientService, + ClientRepository, + AuthService, + JwtService, + UserRepository, + UserSessionRepository, + ], exports: [ClientService], }) export class ClientModule {} diff --git a/src/client/controllers/client.controller.ts b/src/client/controllers/client.controller.ts index edf22ea..22d028d 100644 --- a/src/client/controllers/client.controller.ts +++ b/src/client/controllers/client.controller.ts @@ -1,15 +1,33 @@ import { Controller, Post, Body } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { RegisterClientDto } from '../dtos/register-client.dto'; import { ClientService } from '../services'; +import { ControllerRoute } from '@app/common/constants/controller-route'; +import { ClientTokenDto } from '../dtos/token-client.dto'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; @ApiTags('OAuth Clients') -@Controller('clients') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.CLIENT.ROUTE, +}) export class ClientController { constructor(private readonly clientService: ClientService) {} @Post('register') + @ApiOperation({ + summary: ControllerRoute.CLIENT.ACTIONS.REGISTER_NEW_CLIENT_SUMMARY, + description: ControllerRoute.CLIENT.ACTIONS.REGISTER_NEW_CLIENT_DESCRIPTION, + }) async registerClient(@Body() dto: RegisterClientDto) { return this.clientService.registerClient(dto); } + @Post('token') + @ApiOperation({ + summary: ControllerRoute.CLIENT.ACTIONS.LOGIN_CLIENT_SUMMARY, + description: ControllerRoute.CLIENT.ACTIONS.LOGIN_CLIENT_DESCRIPTION, + }) + async login(@Body() dto: ClientTokenDto) { + return this.clientService.loginWithClientCredentials(dto); + } } diff --git a/src/client/dtos/index.ts b/src/client/dtos/index.ts index 34d8bbb..1bde847 100644 --- a/src/client/dtos/index.ts +++ b/src/client/dtos/index.ts @@ -1,3 +1 @@ -export * from './add.community.dto'; -export * from './project.param.dto'; -export * from './get.community.dto'; +export * from './register-client.dto'; diff --git a/src/client/dtos/token-client.dto.ts b/src/client/dtos/token-client.dto.ts new file mode 100644 index 0000000..6d4a7e4 --- /dev/null +++ b/src/client/dtos/token-client.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class ClientTokenDto { + @ApiProperty({ + example: 'abcd1234xyz', + description: 'The client ID of the client', + required: true, + }) + @IsString() + @IsNotEmpty() + clientId: string; + + @ApiProperty({ + example: 'secureSecret123', + description: 'The client secret of the client', + required: true, + }) + @IsString() + @IsNotEmpty() + clientSecret: string; +} diff --git a/src/client/services/client.service.ts b/src/client/services/client.service.ts index 795e33a..d335413 100644 --- a/src/client/services/client.service.ts +++ b/src/client/services/client.service.ts @@ -1,18 +1,37 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import * as crypto from 'crypto'; import { RegisterClientDto } from '../dtos/register-client.dto'; import { ClientEntity } from '@app/common/modules/client/entities'; +import { ClientTokenDto } from '../dtos/token-client.dto'; +import { AuthService } from '@app/common/auth/services/auth.service'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; @Injectable() export class ClientService { constructor( @InjectRepository(ClientEntity) private clientRepository: Repository, + private readonly authService: AuthService, ) {} - + async loginWithClientCredentials(dto: ClientTokenDto) { + const client = await this.validateClient(dto.clientId, dto.clientSecret); + const payload = { + client: { + clientId: client.clientId, + uuid: client.uuid, + scopes: client.scopes, + }, + }; + const tokens = await this.authService.getTokens(payload, false, '5m'); + return new SuccessResponseDto({ + message: `Client logged in successfully`, + data: tokens, + statusCode: HttpStatus.CREATED, + }); + } async registerClient(dto: RegisterClientDto) { const clientId = crypto.randomBytes(16).toString('hex'); const clientSecret = crypto.randomBytes(32).toString('hex'); @@ -27,7 +46,11 @@ export class ClientService { await this.clientRepository.save(client); - return { clientId, clientSecret }; + return new SuccessResponseDto({ + message: `Client registered successfully`, + data: { clientId, clientSecret }, + statusCode: HttpStatus.CREATED, + }); } async validateClient( diff --git a/src/guards/client.guard.ts b/src/guards/client.guard.ts new file mode 100644 index 0000000..c24a04d --- /dev/null +++ b/src/guards/client.guard.ts @@ -0,0 +1,49 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import * as jwt from 'jsonwebtoken'; + +export class ClientGuard extends AuthGuard('jwt') { + handleRequest(err, user, info, context) { + if (err || !user) { + throw err || new UnauthorizedException(); + } + + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + + if (!token) { + throw new UnauthorizedException('Token not found'); + } + + try { + const decoded = jwt.decode(token); + if (!this.validateToken(decoded)) { + throw new UnauthorizedException('Invalid token'); + } + } catch (err) { + throw new UnauthorizedException('Invalid token'); + } + + return user; + } + + private extractTokenFromHeader(request): string | null { + const authHeader = request.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + return authHeader.split(' ')[1]; + } + return null; + } + + private validateToken(decoded: any): boolean { + if ( + decoded && + decoded.client && + decoded.client.clientId && + decoded.client.uuid + ) { + return true; + } + return false; + } +} From cf8cf08bdc007c5dc8c0242d77aa942273ba89bc Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Thu, 27 Mar 2025 01:55:26 +0300 Subject: [PATCH 4/4] add client relation btw user and client --- .../src/modules/client/entities/client.entity.ts | 6 +++++- libs/common/src/modules/user/entities/user.entity.ts | 8 ++++++++ src/auth/controllers/user-auth.controller.ts | 8 ++++++-- src/auth/services/user-auth.service.ts | 6 +++++- src/guards/client.guard.ts | 12 +++--------- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/libs/common/src/modules/client/entities/client.entity.ts b/libs/common/src/modules/client/entities/client.entity.ts index 81c7a9b..fafcb86 100644 --- a/libs/common/src/modules/client/entities/client.entity.ts +++ b/libs/common/src/modules/client/entities/client.entity.ts @@ -1,6 +1,7 @@ -import { Entity, Column, Unique } from 'typeorm'; +import { Entity, Column, Unique, OneToMany } from 'typeorm'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { ClientDto } from '../dtos'; +import { UserEntity } from '../../user/entities'; @Entity({ name: 'clients' }) @Unique(['clientId']) @@ -39,4 +40,7 @@ export class ClientEntity extends AbstractEntity { @Column('simple-array') scopes: string[]; + + @OneToMany(() => UserEntity, (user) => user.client) + users: UserEntity[]; } diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index f5a9d89..e63fddf 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -29,6 +29,7 @@ import { VisitorPasswordEntity } from '../../visitor-password/entities'; import { InviteUserEntity } from '../../Invite-user/entities'; import { ProjectEntity } from '../../project/entities'; import { SpaceEntity } from '../../space/entities/space.entity'; +import { ClientEntity } from '../../client/entities'; @Entity({ name: 'user' }) export class UserEntity extends AbstractEntity { @@ -143,6 +144,13 @@ export class UserEntity extends AbstractEntity { }) @JoinColumn({ name: 'project_uuid' }) public project: ProjectEntity; + + @ManyToOne(() => ClientEntity, (client) => client.users, { + nullable: true, + }) + @JoinColumn({ name: 'client_uuid' }) + public client: ClientEntity; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/src/auth/controllers/user-auth.controller.ts b/src/auth/controllers/user-auth.controller.ts index 675c264..4fc86c3 100644 --- a/src/auth/controllers/user-auth.controller.ts +++ b/src/auth/controllers/user-auth.controller.ts @@ -37,8 +37,12 @@ export class UserAuthController { summary: ControllerRoute.AUTHENTICATION.ACTIONS.SIGN_UP_SUMMARY, description: ControllerRoute.AUTHENTICATION.ACTIONS.SIGN_UP_DESCRIPTION, }) - async signUp(@Body() userSignUpDto: UserSignUpDto) { - const signupUser = await this.userAuthService.signUp(userSignUpDto); + async signUp(@Body() userSignUpDto: UserSignUpDto, @Req() req: any) { + const clientUuid = req.client.uuid; + const signupUser = await this.userAuthService.signUp( + userSignUpDto, + clientUuid, + ); return { statusCode: HttpStatus.CREATED, data: { diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index e4a6551..eea8285 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -34,7 +34,10 @@ export class UserAuthService { private readonly configService: ConfigService, ) {} - async signUp(userSignUpDto: UserSignUpDto): Promise { + async signUp( + userSignUpDto: UserSignUpDto, + clientUuid?: string, + ): Promise { const findUser = await this.findUser(userSignUpDto.email); if (findUser) { @@ -63,6 +66,7 @@ export class UserAuthService { hasAcceptedAppAgreement, password: hashedPassword, roleType: { uuid: spaceMemberRole.uuid }, + client: { uuid: clientUuid }, region: regionUuid ? { uuid: regionUuid, diff --git a/src/guards/client.guard.ts b/src/guards/client.guard.ts index c24a04d..f2e7b4e 100644 --- a/src/guards/client.guard.ts +++ b/src/guards/client.guard.ts @@ -20,6 +20,8 @@ export class ClientGuard extends AuthGuard('jwt') { if (!this.validateToken(decoded)) { throw new UnauthorizedException('Invalid token'); } + + request.client = (decoded as jwt.JwtPayload).client; } catch (err) { throw new UnauthorizedException('Invalid token'); } @@ -36,14 +38,6 @@ export class ClientGuard extends AuthGuard('jwt') { } private validateToken(decoded: any): boolean { - if ( - decoded && - decoded.client && - decoded.client.clientId && - decoded.client.uuid - ) { - return true; - } - return false; + return decoded?.client?.clientId && decoded?.client?.uuid; } }