mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-11-26 11:34:53 +00:00
Merge pull request #325 from SyncrowIOT/SP-1306-be-implement-client-credential-flow-that-accept-client-id-and-client-secret-from-3rd-party-applications-and-issue-o-auth-jwt-tokens
SP-1306-be-implement-client-credential-flow-that-accept-client-id-and-client-secret-from-3rd-party-applications-and-issue-o-auth-jwt-tokens
This commit is contained in:
@ -77,21 +77,28 @@ export class AuthService {
|
|||||||
return await this.sessionRepository.save(data);
|
return await this.sessionRepository.save(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTokens(payload) {
|
async getTokens(
|
||||||
|
payload,
|
||||||
|
isRefreshToken = true,
|
||||||
|
accessTokenExpiry = '24h',
|
||||||
|
refreshTokenExpiry = '30d',
|
||||||
|
) {
|
||||||
const [accessToken, refreshToken] = await Promise.all([
|
const [accessToken, refreshToken] = await Promise.all([
|
||||||
this.jwtService.signAsync(payload, {
|
this.jwtService.signAsync(payload, {
|
||||||
secret: this.configService.get<string>('JWT_SECRET'),
|
secret: this.configService.get<string>('JWT_SECRET'),
|
||||||
expiresIn: '24h',
|
expiresIn: accessTokenExpiry,
|
||||||
}),
|
|
||||||
this.jwtService.signAsync(payload, {
|
|
||||||
secret: this.configService.get<string>('JWT_SECRET'),
|
|
||||||
expiresIn: '7d',
|
|
||||||
}),
|
}),
|
||||||
|
isRefreshToken
|
||||||
|
? this.jwtService.signAsync(payload, {
|
||||||
|
secret: this.configService.get<string>('JWT_SECRET'),
|
||||||
|
expiresIn: refreshTokenExpiry,
|
||||||
|
})
|
||||||
|
: null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
...(isRefreshToken ? { refreshToken } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,17 @@
|
|||||||
export class ControllerRoute {
|
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 {
|
static PROJECT = class {
|
||||||
public static readonly ROUTE = 'projects';
|
public static readonly ROUTE = 'projects';
|
||||||
static ACTIONS = class {
|
static ACTIONS = class {
|
||||||
|
|||||||
@ -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 { SubspaceProductAllocationEntity } from '../modules/space/entities/subspace/subspace-product-allocation.entity';
|
||||||
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
|
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
|
||||||
import { TagEntity } from '../modules/space/entities/tag.entity';
|
import { TagEntity } from '../modules/space/entities/tag.entity';
|
||||||
|
import { ClientEntity } from '../modules/client/entities';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
@ -93,6 +94,7 @@ import { TagEntity } from '../modules/space/entities/tag.entity';
|
|||||||
SubspaceModelProductAllocationEntity,
|
SubspaceModelProductAllocationEntity,
|
||||||
SpaceProductAllocationEntity,
|
SpaceProductAllocationEntity,
|
||||||
SubspaceProductAllocationEntity,
|
SubspaceProductAllocationEntity,
|
||||||
|
ClientEntity,
|
||||||
],
|
],
|
||||||
namingStrategy: new SnakeNamingStrategy(),
|
namingStrategy: new SnakeNamingStrategy(),
|
||||||
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
|
||||||
|
|||||||
@ -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 {}
|
||||||
20
libs/common/src/modules/client/dtos/client.dto.ts
Normal file
20
libs/common/src/modules/client/dtos/client.dto.ts
Normal file
@ -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[];
|
||||||
|
}
|
||||||
1
libs/common/src/modules/client/dtos/index.ts
Normal file
1
libs/common/src/modules/client/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './client.dto';
|
||||||
46
libs/common/src/modules/client/entities/client.entity.ts
Normal file
46
libs/common/src/modules/client/entities/client.entity.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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'])
|
||||||
|
export class ClientEntity extends AbstractEntity<ClientDto> {
|
||||||
|
@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[];
|
||||||
|
|
||||||
|
@OneToMany(() => UserEntity, (user) => user.client)
|
||||||
|
users: UserEntity[];
|
||||||
|
}
|
||||||
1
libs/common/src/modules/client/entities/index.ts
Normal file
1
libs/common/src/modules/client/entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './client.entity';
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ClientEntity } from '../entities';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ClientRepository extends Repository<ClientEntity> {
|
||||||
|
constructor(private dataSource: DataSource) {
|
||||||
|
super(ClientEntity, dataSource.createEntityManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/common/src/modules/client/repositories/index.ts
Normal file
1
libs/common/src/modules/client/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './client.repository';
|
||||||
@ -29,6 +29,7 @@ import { VisitorPasswordEntity } from '../../visitor-password/entities';
|
|||||||
import { InviteUserEntity } from '../../Invite-user/entities';
|
import { InviteUserEntity } from '../../Invite-user/entities';
|
||||||
import { ProjectEntity } from '../../project/entities';
|
import { ProjectEntity } from '../../project/entities';
|
||||||
import { SpaceEntity } from '../../space/entities/space.entity';
|
import { SpaceEntity } from '../../space/entities/space.entity';
|
||||||
|
import { ClientEntity } from '../../client/entities';
|
||||||
|
|
||||||
@Entity({ name: 'user' })
|
@Entity({ name: 'user' })
|
||||||
export class UserEntity extends AbstractEntity<UserDto> {
|
export class UserEntity extends AbstractEntity<UserDto> {
|
||||||
@ -143,6 +144,13 @@ export class UserEntity extends AbstractEntity<UserDto> {
|
|||||||
})
|
})
|
||||||
@JoinColumn({ name: 'project_uuid' })
|
@JoinColumn({ name: 'project_uuid' })
|
||||||
public project: ProjectEntity;
|
public project: ProjectEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => ClientEntity, (client) => client.users, {
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'client_uuid' })
|
||||||
|
public client: ClientEntity;
|
||||||
|
|
||||||
constructor(partial: Partial<UserEntity>) {
|
constructor(partial: Partial<UserEntity>) {
|
||||||
super();
|
super();
|
||||||
Object.assign(this, partial);
|
Object.assign(this, partial);
|
||||||
|
|||||||
@ -29,11 +29,13 @@ import { RoleModule } from './role/role.module';
|
|||||||
import { TermsConditionsModule } from './terms-conditions/terms-conditions.module';
|
import { TermsConditionsModule } from './terms-conditions/terms-conditions.module';
|
||||||
import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
|
import { PrivacyPolicyModule } from './privacy-policy/privacy-policy.module';
|
||||||
import { TagModule } from './tags/tags.module';
|
import { TagModule } from './tags/tags.module';
|
||||||
|
import { ClientModule } from './client/client.module';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
load: config,
|
load: config,
|
||||||
}),
|
}),
|
||||||
|
ClientModule,
|
||||||
AuthenticationModule,
|
AuthenticationModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
InviteUserModule,
|
InviteUserModule,
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
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 { UserRepository } from '@app/common/modules/user/repositories';
|
||||||
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
|
import { UserSessionRepository } from '@app/common/modules/session/repositories/session.repository';
|
||||||
import { UserOtpRepository } from '@app/common/modules/user/repositories';
|
import { UserOtpRepository } from '@app/common/modules/user/repositories';
|
||||||
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
|
import { RoleTypeRepository } from '@app/common/modules/role-type/repositories';
|
||||||
import { RoleService } from 'src/role/services';
|
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({
|
@Module({
|
||||||
imports: [ConfigModule, UserRepositoryModule, CommonModule],
|
imports: [ConfigModule],
|
||||||
controllers: [UserAuthController],
|
controllers: [UserAuthController],
|
||||||
providers: [
|
providers: [
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
@ -20,6 +21,9 @@ import { RoleService } from 'src/role/services';
|
|||||||
UserOtpRepository,
|
UserOtpRepository,
|
||||||
RoleTypeRepository,
|
RoleTypeRepository,
|
||||||
RoleService,
|
RoleService,
|
||||||
|
AuthService,
|
||||||
|
EmailService,
|
||||||
|
JwtService,
|
||||||
],
|
],
|
||||||
exports: [UserAuthService],
|
exports: [UserAuthService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard';
|
|||||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||||
import { OtpType } from '@app/common/constants/otp-type.enum';
|
import { OtpType } from '@app/common/constants/otp-type.enum';
|
||||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||||
|
import { ClientGuard } from 'src/guards/client.guard';
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
version: EnableDisableStatusEnum.ENABLED,
|
version: EnableDisableStatusEnum.ENABLED,
|
||||||
@ -29,13 +30,19 @@ export class UserAuthController {
|
|||||||
constructor(private readonly userAuthService: UserAuthService) {}
|
constructor(private readonly userAuthService: UserAuthService) {}
|
||||||
|
|
||||||
@ResponseMessage('User Registered Successfully')
|
@ResponseMessage('User Registered Successfully')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(ClientGuard)
|
||||||
@Post('user/signup')
|
@Post('user/signup')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: ControllerRoute.AUTHENTICATION.ACTIONS.SIGN_UP_SUMMARY,
|
summary: ControllerRoute.AUTHENTICATION.ACTIONS.SIGN_UP_SUMMARY,
|
||||||
description: ControllerRoute.AUTHENTICATION.ACTIONS.SIGN_UP_DESCRIPTION,
|
description: ControllerRoute.AUTHENTICATION.ACTIONS.SIGN_UP_DESCRIPTION,
|
||||||
})
|
})
|
||||||
async signUp(@Body() userSignUpDto: UserSignUpDto) {
|
async signUp(@Body() userSignUpDto: UserSignUpDto, @Req() req: any) {
|
||||||
const signupUser = await this.userAuthService.signUp(userSignUpDto);
|
const clientUuid = req.client.uuid;
|
||||||
|
const signupUser = await this.userAuthService.signUp(
|
||||||
|
userSignUpDto,
|
||||||
|
clientUuid,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
statusCode: HttpStatus.CREATED,
|
statusCode: HttpStatus.CREATED,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@ -34,7 +34,10 @@ export class UserAuthService {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async signUp(userSignUpDto: UserSignUpDto): Promise<UserEntity> {
|
async signUp(
|
||||||
|
userSignUpDto: UserSignUpDto,
|
||||||
|
clientUuid?: string,
|
||||||
|
): Promise<UserEntity> {
|
||||||
const findUser = await this.findUser(userSignUpDto.email);
|
const findUser = await this.findUser(userSignUpDto.email);
|
||||||
|
|
||||||
if (findUser) {
|
if (findUser) {
|
||||||
@ -63,6 +66,7 @@ export class UserAuthService {
|
|||||||
hasAcceptedAppAgreement,
|
hasAcceptedAppAgreement,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
roleType: { uuid: spaceMemberRole.uuid },
|
roleType: { uuid: spaceMemberRole.uuid },
|
||||||
|
client: { uuid: clientUuid },
|
||||||
region: regionUuid
|
region: regionUuid
|
||||||
? {
|
? {
|
||||||
uuid: regionUuid,
|
uuid: regionUuid,
|
||||||
|
|||||||
24
src/client/client.module.ts
Normal file
24
src/client/client.module.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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';
|
||||||
|
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,
|
||||||
|
AuthService,
|
||||||
|
JwtService,
|
||||||
|
UserRepository,
|
||||||
|
UserSessionRepository,
|
||||||
|
],
|
||||||
|
exports: [ClientService],
|
||||||
|
})
|
||||||
|
export class ClientModule {}
|
||||||
33
src/client/controllers/client.controller.ts
Normal file
33
src/client/controllers/client.controller.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Controller, Post, Body } from '@nestjs/common';
|
||||||
|
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({
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/client/controllers/index.ts
Normal file
1
src/client/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './client.controller';
|
||||||
1
src/client/dtos/index.ts
Normal file
1
src/client/dtos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './register-client.dto';
|
||||||
31
src/client/dtos/register-client.dto.ts
Normal file
31
src/client/dtos/register-client.dto.ts
Normal file
@ -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[];
|
||||||
|
}
|
||||||
22
src/client/dtos/token-client.dto.ts
Normal file
22
src/client/dtos/token-client.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
68
src/client/services/client.service.ts
Normal file
68
src/client/services/client.service.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
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<ClientEntity>,
|
||||||
|
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');
|
||||||
|
|
||||||
|
const client = this.clientRepository.create({
|
||||||
|
name: dto.name,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
redirectUri: dto.redirectUri,
|
||||||
|
scopes: dto.scopes,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.clientRepository.save(client);
|
||||||
|
|
||||||
|
return new SuccessResponseDto({
|
||||||
|
message: `Client registered successfully`,
|
||||||
|
data: { clientId, clientSecret },
|
||||||
|
statusCode: HttpStatus.CREATED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateClient(
|
||||||
|
clientId: string,
|
||||||
|
clientSecret: string,
|
||||||
|
): Promise<ClientEntity | null> {
|
||||||
|
const client = await this.clientRepository.findOne({
|
||||||
|
where: { clientId, clientSecret },
|
||||||
|
});
|
||||||
|
if (!client) {
|
||||||
|
throw new NotFoundException('Invalid client credentials');
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/client/services/index.ts
Normal file
1
src/client/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './client.service';
|
||||||
43
src/guards/client.guard.ts
Normal file
43
src/guards/client.guard.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
request.client = (decoded as jwt.JwtPayload).client;
|
||||||
|
} 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 {
|
||||||
|
return decoded?.client?.clientId && decoded?.client?.uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user