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