Skip to content

Commit

Permalink
refactor: add azure usecases
Browse files Browse the repository at this point in the history
  • Loading branch information
nunocaseiro committed Mar 13, 2023
1 parent 7f22dec commit b0f1beb
Show file tree
Hide file tree
Showing 21 changed files with 272 additions and 221 deletions.
3 changes: 2 additions & 1 deletion backend/src/modules/auth/auth.providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ValidateUserEmailUseCase from './applications/validate-user-email.use-cas
import { TYPES } from './interfaces/types';
import { ResetPasswordRepository } from './repository/reset-password.repository';
import GetTokenAuthService from './services/get-token.auth.service';
import ValidateUserAuthService from './services/validate-user.auth.service';

export const getTokenAuthService = {
provide: TYPES.services.GetTokenAuthService,
Expand All @@ -17,7 +18,7 @@ export const getTokenAuthService = {

export const validateUserAuthService = {
provide: TYPES.services.ValidateAuthService,
useClass: ValidateUserEmailUseCase
useClass: ValidateUserAuthService
};

export const registerUserUseCase = {
Expand Down
61 changes: 37 additions & 24 deletions backend/src/modules/auth/controller/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import * as request from 'supertest';
import mockedUser from 'src/libs/test-utils/mocks/user.mock';
import AuthController from 'src/modules/auth/controller/auth.controller';
import EmailModule from 'src/modules/mailer/mailer.module';
import * as Auth from 'src/modules/auth/interfaces/types';
import * as Teams from 'src/modules/teams/interfaces/types';
import * as Boards from 'src/modules/boards/interfaces/types';
import * as User from 'src/modules/users/interfaces/types';
import { RegisterUserUseCaseInterface } from '../interfaces/applications/register-user.use-case.interface';
import { RegisterGuestUserUseCaseInterface } from '../interfaces/applications/register-guest-user.use-case.interface';
import { ValidateUserEmailUseCaseInterface } from '../interfaces/applications/validate-email.use-case.interface';
import { RefreshTokenUseCaseInterface } from '../interfaces/applications/refresh-token.use-case.interface';
import { StatisticsAuthUserUseCaseInterface } from '../interfaces/applications/statistics.auth.use-case.interface';
import { ResetPasswordUseCaseInterface } from '../interfaces/applications/reset-password.use-case.interface';
import { CreateResetTokenUseCaseInterface } from '../interfaces/applications/create-reset-token.use-case.interface';
import { SignInUseCaseInterface } from '../interfaces/applications/signIn.use-case.interface';
import { TYPES } from '../interfaces/types';
import { createMock } from '@golevelup/ts-jest';
import { RegisterAuthApplication } from '../applications/register.auth.application';
import { GetTeamApplication } from 'src/modules/teams/applications/get.team.application';
import { CreateResetTokenAuthApplication } from '../applications/create-reset-token.use-case';
import { UpdateUserApplication } from 'src/modules/users/use-cases/update.user.application';
import { GetBoardApplication } from 'src/modules/boards/applications/get.board.application';
import { GetUserApplication } from 'src/modules/users/use-cases/get.user.application';
import { ResetPasswordRepositoryInterface } from '../repository/reset-password.repository.interface';
import { getModelToken } from '@nestjs/mongoose';

describe('AuthController', () => {
let app: INestApplication;
Expand All @@ -26,32 +27,44 @@ describe('AuthController', () => {
controllers: [AuthController],
providers: [
{
provide: Auth.TYPES.applications.RegisterAuthApplication,
useValue: createMock<RegisterAuthApplication>()
provide: TYPES.applications.RegisterUserUseCase,
useValue: createMock<RegisterUserUseCaseInterface>()
},
{
provide: Auth.TYPES.applications.GetTokenAuthApplication,
useValue: createMock<GetTeamApplication>()
provide: TYPES.applications.RegisterGuestUserUseCase,
useValue: createMock<RegisterGuestUserUseCaseInterface>()
},
{
provide: Auth.TYPES.applications.CreateResetTokenAuthApplication,
useValue: createMock<CreateResetTokenAuthApplication>()
provide: TYPES.applications.ValidateUserEmailUseCase,
useValue: createMock<ValidateUserEmailUseCaseInterface>()
},
{
provide: Auth.TYPES.applications.UpdateUserApplication,
useValue: createMock<UpdateUserApplication>()
provide: TYPES.applications.RefreshTokenUseCase,
useValue: createMock<RefreshTokenUseCaseInterface>()
},
{
provide: Teams.TYPES.applications.GetTeamApplication,
useValue: createMock<GetTeamApplication>()
provide: TYPES.applications.StatisticsAuthUserUseCase,
useValue: createMock<StatisticsAuthUserUseCaseInterface>()
},
{
provide: Boards.TYPES.applications.GetBoardApplication,
useValue: createMock<GetBoardApplication>()
provide: TYPES.applications.ResetPasswordUseCase,
useValue: createMock<ResetPasswordUseCaseInterface>()
},
{
provide: User.TYPES.applications.GetUserApplication,
useValue: createMock<GetUserApplication>()
provide: TYPES.applications.CreateResetTokenUseCase,
useValue: createMock<CreateResetTokenUseCaseInterface>()
},
{
provide: TYPES.applications.SignInUseCase,
useValue: createMock<SignInUseCaseInterface>()
},
{
provide: getModelToken('ResetPassword'),
useValue: {}
},
{
provide: TYPES.repository.ResetPasswordRepository,
useValue: createMock<ResetPasswordRepositoryInterface>()
}
]
}).compile();
Expand Down
5 changes: 1 addition & 4 deletions backend/src/modules/auth/interfaces/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ export const TYPES = {
SignInUseCase: 'SignInUseCase',
RefreshTokenUseCase: 'RefreshTokenUseCase',
ResetPasswordUseCase: 'ResetPasswordUseCase',
CreateResetTokenUseCase: 'CreateResetTokenUseCase',
CreateResetTokenAuthApplication: 'CreateResetTokenAuthApplication',
UpdatePasswordAuthApplication: 'UpdatePasswordAuthApplication',
UpdateUserApplication: 'UpdateUserApplication'
CreateResetTokenUseCase: 'CreateResetTokenUseCase'
},
repository: {
ResetPasswordRepository: 'ResetPasswordRepository'
Expand Down
11 changes: 7 additions & 4 deletions backend/src/modules/auth/services/get-token.auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import configService from 'src/libs/test-utils/mocks/configService.mock';
import jwtService from 'src/libs/test-utils/mocks/jwtService.mock';
import GetTokenAuthService from 'src/modules/auth/services/get-token.auth.service';
import { updateUserService, userRepository } from 'src/modules/users/users.providers';
import { getTokenAuthService, resetPasswordRepository } from '../auth.providers';
import { GetTokenAuthServiceInterface } from '../interfaces/services/get-token.auth.service.interface';
import { TYPES } from '../interfaces/types';

describe('AuthService', () => {
let service: GetTokenAuthService;
let service: GetTokenAuthServiceInterface;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
GetTokenAuthService,
getTokenAuthService,
updateUserService,
userRepository,
resetPasswordRepository,
{
provide: ConfigService,
useValue: configService
Expand All @@ -35,7 +38,7 @@ describe('AuthService', () => {
]
}).compile();

service = module.get<GetTokenAuthService>(GetTokenAuthService);
service = module.get<GetTokenAuthServiceInterface>(TYPES.services.GetTokenAuthService);
});

describe('when creating a jwt', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
updateUserService,
userRepository
} from 'src/modules/users/users.providers';
import { getTokenAuthService } from '../auth.providers';
import { getTokenAuthService, resetPasswordRepository } from '../auth.providers';

jest.mock('bcrypt');
jest.mock('src/modules/schedules/services/create.schedules.service.ts');
Expand Down Expand Up @@ -68,6 +68,7 @@ describe('The AuthenticationService', () => {
boardRepository,
updateUserService,
getBoardUserService,
resetPasswordRepository,
{
provide: ConfigService,
useValue: configService
Expand Down
20 changes: 0 additions & 20 deletions backend/src/modules/azure/applications/auth.azure.application.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
import { AuthAzureServiceInterface } from '../interfaces/services/auth.azure.service.interface';
import { TYPES } from '../interfaces/types';
import * as UserType from 'src/modules/users/interfaces/types';
import { GetUserServiceInterface } from 'src/modules/users/interfaces/services/get.user.service.interface';
import { CheckUserAzureUseCaseInterface } from '../interfaces/applications/check-user.azure.use-case.interface';
import { USER_NOT_FOUND } from 'src/libs/exceptions/messages';

@Injectable()
export class CheckUserAzureUseCase implements CheckUserAzureUseCaseInterface {
constructor(
@Inject(TYPES.services.AuthAzureService)
private authAzureService: AuthAzureServiceInterface,
@Inject(UserType.TYPES.services.GetUserService)
private readonly getUserService: GetUserServiceInterface
) {}

async execute(email: string) {
const existUserInAzure = await this.authAzureService.getUserFromAzure(email);

if (existUserInAzure) {
return 'az';
}

const existUserInDB = await this.getUserService.getByEmail(email);

if (existUserInDB) {
return 'local';
}

throw new NotFoundException(USER_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Inject, Injectable } from '@nestjs/common';
import { RegisterOrLoginAzureUseCaseInterface } from '../interfaces/applications/register-or-login.azure.use-case.interface';
import { AuthAzureServiceInterface } from '../interfaces/services/auth.azure.service.interface';
import { TYPES } from '../interfaces/types';
import { AzureDecodedUser } from '../services/auth.azure.service';
import jwt_decode from 'jwt-decode';
import * as UserType from 'src/modules/users/interfaces/types';
import { GetUserServiceInterface } from 'src/modules/users/interfaces/services/get.user.service.interface';
import { CreateUserServiceInterface } from 'src/modules/users/interfaces/services/create.user.service.interface';
import User from 'src/modules/users/entities/user.schema';
import * as AuthType from 'src/modules/auth/interfaces/types';
import * as StorageType from 'src/modules/storage/interfaces/types';
import { UpdateUserServiceInterface } from 'src/modules/users/interfaces/services/update.user.service.interface';
import { GetTokenAuthServiceInterface } from 'src/modules/auth/interfaces/services/get-token.auth.service.interface';
import { signIn } from 'src/modules/auth/shared/login.auth';
import { createHash } from 'node:crypto';
import { StorageServiceInterface } from 'src/modules/storage/interfaces/services/storage.service';

@Injectable()
export class RegisterOrLoginAzureUseCase implements RegisterOrLoginAzureUseCaseInterface {
constructor(
@Inject(TYPES.services.AuthAzureService)
private authAzureService: AuthAzureServiceInterface,
@Inject(UserType.TYPES.services.GetUserService)
private readonly getUserService: GetUserServiceInterface,
@Inject(UserType.TYPES.services.CreateUserService)
private readonly createUserService: CreateUserServiceInterface,
@Inject(AuthType.TYPES.services.UpdateUserService)
private readonly updateUserService: UpdateUserServiceInterface,
@Inject(AuthType.TYPES.services.GetTokenAuthService)
private readonly getTokenService: GetTokenAuthServiceInterface,
@Inject(StorageType.TYPES.services.StorageService)
private readonly storageService: StorageServiceInterface
) {}

async execute(azureToken: string) {
const { unique_name, email, name, given_name, family_name } = <AzureDecodedUser>(
jwt_decode(azureToken)
);

const splitedName = name ? name.split(' ') : [];
const firstName = given_name ?? splitedName[0] ?? 'first';
const lastName = family_name ?? splitedName[splitedName.length - 1] ?? 'last';

const emailOrUniqueName = email ?? unique_name;

const userFromAzure = await this.authAzureService.getUserFromAzure(emailOrUniqueName);

if (!userFromAzure) return null;

const user = await this.getUserService.getByEmail(emailOrUniqueName);

let userToAuthenticate: User;

if (user) {
userToAuthenticate = user;
} else {
const createdUser = await this.createUserService.create({
email: emailOrUniqueName,
firstName,
lastName,
providerAccountCreatedAt: userFromAzure.createdDateTime
});

if (!createdUser) return null;

userToAuthenticate = createdUser;
}

const avatarUrl = await this.getUserPhoto(userToAuthenticate);

if (avatarUrl) {
await this.updateUserService.updateUserAvatar(userToAuthenticate._id, avatarUrl);

userToAuthenticate.avatar = avatarUrl;
}

return signIn(userToAuthenticate, this.getTokenService, 'azure');
}

private async getUserPhoto(user: User) {
const { email, avatar } = user;
const azureUser = await this.authAzureService.getUserFromAzure(email);

if (!azureUser) return '';

try {
const blob = await this.authAzureService
.getGraphClient()
.api(`/users/${azureUser.id}/photo/$value`)
.get();

const buffer = Buffer.from(await blob.arrayBuffer());
const hash = createHash('md5').update(buffer).digest('hex');

if (avatar) {
const avatarHash = avatar.split('/').pop().split('.').shift();

if (avatarHash === hash) return;

await this.storageService.deleteFile(avatar);
}

return this.storageService.uploadFile(hash, {
buffer,
mimetype: blob.type,
originalname: `${hash}.${blob.type.split('/').pop()}`
});
} catch (ex) {
return '';
}
}
}
4 changes: 2 additions & 2 deletions backend/src/modules/azure/azure.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import AuthModule from '../auth/auth.module';
import { CommunicationModule } from '../communication/communication.module';
import { StorageModule } from '../storage/storage.module';
import UsersModule from '../users/users.module';
import { authAzureApplication, authAzureService } from './azure.providers';
import { authAzureService, checkUserUseCase, registerOrLoginUseCase } from './azure.providers';
import AzureController from './controller/azure.controller';

@Module({
imports: [UsersModule, AuthModule, CommunicationModule, StorageModule],
controllers: [AzureController],
providers: [authAzureService, authAzureApplication]
providers: [authAzureService, checkUserUseCase, registerOrLoginUseCase]
})
export default class AzureModule {}
14 changes: 10 additions & 4 deletions backend/src/modules/azure/azure.providers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AuthAzureApplication } from './applications/auth.azure.application';
import { CheckUserAzureUseCase } from './applications/check-user.azure.use-case';
import { RegisterOrLoginAzureUseCase } from './applications/register-or-login.azure.use-case';
import { TYPES } from './interfaces/types';
import AuthAzureService from './services/auth.azure.service';

Expand All @@ -7,7 +8,12 @@ export const authAzureService = {
useClass: AuthAzureService
};

export const authAzureApplication = {
provide: TYPES.applications.AuthAzureApplication,
useClass: AuthAzureApplication
export const checkUserUseCase = {
provide: TYPES.applications.CheckUserUseCase,
useClass: CheckUserAzureUseCase
};

export const registerOrLoginUseCase = {
provide: TYPES.applications.RegisterOrLoginUseCase,
useClass: RegisterOrLoginAzureUseCase
};
Loading

0 comments on commit b0f1beb

Please sign in to comment.