From 359cd2e05f514ba2460bc367cc0f03f36927561a Mon Sep 17 00:00:00 2001 From: Ismail Akintunde Date: Tue, 23 Jul 2024 12:35:07 +0100 Subject: [PATCH] fix: prunned folder directories --- config/appConfig.ts | 11 ++ package.json | 8 +- src/app.module.ts | 6 +- src/database/seeding/seeding.controller.ts | 17 +-- src/database/seeding/seeding.module.ts | 9 +- src/database/seeding/seeding.service.ts | 98 +--------------- src/entities/organisation.entity.ts | 17 --- src/entities/parent.entity.ts | 13 +++ src/entities/product.entity.ts | 21 ---- src/entities/profile.entity.ts | 23 ---- src/entities/user.entity.ts | 70 ----------- src/helpers/SystemMessages.ts | 5 + src/helpers/skipAuth.ts | 4 + src/main.ts | 2 +- src/modules/auth/auth.controller.ts | 19 +++ src/modules/auth/auth.module.ts | 26 +++++ src/modules/auth/auth.service.ts | 69 +++++++++++ src/modules/auth/dto/create-user.dto.ts | 21 ++++ src/modules/auth/dto/user-resonse.dto.ts | 4 + src/modules/auth/tests/auth.service.spec.ts | 109 ++++++++++++++++++ src/modules/user/dto/user-response.dto.ts | 4 + src/modules/user/entities/user.entity.ts | 40 +++++++ src/modules/user/interfaces/UserInterface.ts | 24 ++++ .../user/options/CreateNewUserOptions.ts | 4 + .../user/options/UserIdentifierOptions.ts | 10 ++ src/modules/user/user.module.ts | 18 +++ src/modules/user/user.service.ts | 44 +++++++ 27 files changed, 445 insertions(+), 251 deletions(-) create mode 100644 config/appConfig.ts delete mode 100644 src/entities/organisation.entity.ts create mode 100644 src/entities/parent.entity.ts delete mode 100644 src/entities/product.entity.ts delete mode 100644 src/entities/profile.entity.ts delete mode 100644 src/entities/user.entity.ts create mode 100644 src/helpers/SystemMessages.ts create mode 100644 src/helpers/skipAuth.ts create mode 100644 src/modules/auth/auth.controller.ts create mode 100644 src/modules/auth/auth.module.ts create mode 100644 src/modules/auth/auth.service.ts create mode 100644 src/modules/auth/dto/create-user.dto.ts create mode 100644 src/modules/auth/dto/user-resonse.dto.ts create mode 100644 src/modules/auth/tests/auth.service.spec.ts create mode 100644 src/modules/user/dto/user-response.dto.ts create mode 100644 src/modules/user/entities/user.entity.ts create mode 100644 src/modules/user/interfaces/UserInterface.ts create mode 100644 src/modules/user/options/CreateNewUserOptions.ts create mode 100644 src/modules/user/options/UserIdentifierOptions.ts create mode 100644 src/modules/user/user.module.ts create mode 100644 src/modules/user/user.service.ts diff --git a/config/appConfig.ts b/config/appConfig.ts new file mode 100644 index 000000000..7ed1bd3ff --- /dev/null +++ b/config/appConfig.ts @@ -0,0 +1,11 @@ +import * as dotenv from 'dotenv'; + +dotenv.config(); + +export const appConfig = { + jwt: { + jwtSecret: process.env.JWT_SECRET, + jwtExpiry: process.env.JWT_EXPIRY_TIMEFRAME + } + +} \ No newline at end of file diff --git a/package.json b/package.json index fda313675..05a4c985c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "node dist/src/main", "dev": "PROFILE=local ./node_modules/.bin/ts-node-dev -r dotenv/config --respawn src/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", @@ -36,6 +36,8 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.3.9", "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", @@ -44,6 +46,7 @@ "class-validator": "^0.14.1", "joi": "^17.6.0", "nestjs-pino": "^4.1.0", + "passport-jwt": "^4.0.1", "pg": "^8.12.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -61,6 +64,7 @@ "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", @@ -72,7 +76,7 @@ "lint-staged": "^15.2.5", "prettier": "^3.0.0", "source-map-support": "^0.5.21", - "supertest": "^6.3.3", + "supertest": "^6.3.4", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", "ts-node": "^10.9.1", diff --git a/src/app.module.ts b/src/app.module.ts index 244cbd549..6580c527c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,8 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import dataSource from './database/data-source'; import { SeedingModule } from './database/seeding/seeding.module'; import HealthController from './health.controller'; +import { AuthModule } from './modules/auth/auth.module'; +import { UserModule } from './modules/user/user.module'; @Module({ providers: [ @@ -52,7 +54,9 @@ import HealthController from './health.controller'; dataSourceFactory: async () => dataSource, }), SeedingModule, + AuthModule, + UserModule ], controllers: [HealthController], }) -export class AppModule {} +export class AppModule { } diff --git a/src/database/seeding/seeding.controller.ts b/src/database/seeding/seeding.controller.ts index 4ca81856a..66cd9de60 100644 --- a/src/database/seeding/seeding.controller.ts +++ b/src/database/seeding/seeding.controller.ts @@ -3,7 +3,7 @@ import { SeedingService } from './seeding.service'; @Controller('seed') export class SeedingController { - constructor(private readonly seedingService: SeedingService) {} + constructor(private readonly seedingService: SeedingService) { } @Post() async seedDatabase() { @@ -15,19 +15,4 @@ export class SeedingController { async getUsers() { return this.seedingService.getUsers(); } - - @Get('profiles') - async getProfiles() { - return this.seedingService.getProfiles(); - } - - @Get('products') - async getProducts() { - return this.seedingService.getProducts(); - } - - @Get('organisations') - async getOrganisations() { - return this.seedingService.getOrganisations(); - } } diff --git a/src/database/seeding/seeding.module.ts b/src/database/seeding/seeding.module.ts index 0cbc35994..b55e93106 100644 --- a/src/database/seeding/seeding.module.ts +++ b/src/database/seeding/seeding.module.ts @@ -2,14 +2,11 @@ import { Module } from '@nestjs/common'; import { SeedingService } from './seeding.service'; import { SeedingController } from './seeding.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { User } from 'src/entities/user.entity'; -import { Profile } from 'src/entities/profile.entity'; -import { Product } from 'src/entities/product.entity'; -import { Organisation } from 'src/entities/organisation.entity'; +import { User } from '../../modules/user/entities/user.entity'; @Module({ - imports: [TypeOrmModule.forFeature([User, Profile, Product, Organisation])], + imports: [TypeOrmModule.forFeature([User])], providers: [SeedingService], controllers: [SeedingController], }) -export class SeedingModule {} +export class SeedingModule { } diff --git a/src/database/seeding/seeding.service.ts b/src/database/seeding/seeding.service.ts index b92641358..66d09fb9b 100644 --- a/src/database/seeding/seeding.service.ts +++ b/src/database/seeding/seeding.service.ts @@ -1,13 +1,11 @@ import { Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { User } from 'src/entities/user.entity'; -import { Profile } from 'src/entities/profile.entity'; -import { Product } from 'src/entities/product.entity'; -import { Organisation } from 'src/entities/organisation.entity'; +import { User } from '../../modules/user/entities/user.entity'; + @Injectable() export class SeedingService { - constructor(private readonly dataSource: DataSource) {} + constructor(private readonly dataSource: DataSource) { } async seedDatabase() { const userRepository = this.dataSource.getRepository(User); @@ -24,9 +22,7 @@ export class SeedingService { await queryRunner.startTransaction(); try { - const profileRepository = this.dataSource.getRepository(Profile); - const productRepository = this.dataSource.getRepository(Product); - const organisationRepository = this.dataSource.getRepository(Organisation); + const u1 = userRepository.create({ first_name: 'John', @@ -48,66 +44,6 @@ export class SeedingService { throw new Error('Failed to create all users'); } - const p1 = profileRepository.create({ - username: 'johnsmith', - bio: 'bio data', - phone: '1234567890', - avatar_image: 'image.png', - user: savedUsers[0], - }); - const p2 = profileRepository.create({ - username: 'janesmith', - bio: 'bio data', - phone: '0987654321', - avatar_image: 'image.png', - user: savedUsers[1], - }); - - await profileRepository.save([p1, p2]); - - const savedProfiles = await profileRepository.find(); - if (savedProfiles.length !== 2) { - throw new Error('Failed to create all profiles'); - } - - const pr1 = productRepository.create({ - product_name: 'Product 1', - description: 'Description 1', - product_price: 100, - user: savedUsers[0], - }); - const pr2 = productRepository.create({ - product_name: 'Product 2', - description: 'Description 2', - product_price: 200, - user: savedUsers[1], - }); - - await productRepository.save([pr1, pr2]); - - const savedProducts = await productRepository.find(); - if (savedProducts.length !== 2) { - throw new Error('Failed to create all products'); - } - - const or1 = organisationRepository.create({ - org_name: 'Org 1', - description: 'Description 1', - users: savedUsers, - }); - const or2 = organisationRepository.create({ - org_name: 'Org 2', - description: 'Description 2', - users: [savedUsers[0]], - }); - - await organisationRepository.save([or1, or2]); - - const savedOrganisations = await organisationRepository.find(); - if (savedOrganisations.length !== 2) { - throw new Error('Failed to create all organisations'); - } - await queryRunner.commitTransaction(); } catch (error) { await queryRunner.rollbackTransaction(); @@ -129,30 +65,4 @@ export class SeedingService { } } - async getProfiles(): Promise { - try { - return this.dataSource.getRepository(Profile).find({ relations: ['user'] }); - } catch (error) { - console.error('Error fetching profiles:', error.message); - throw error; - } - } - - async getProducts(): Promise { - try { - return this.dataSource.getRepository(Product).find({ relations: ['user'] }); - } catch (error) { - console.error('Error fetching products:', error.message); - throw error; - } - } - - async getOrganisations(): Promise { - try { - return this.dataSource.getRepository(Organisation).find({ relations: ['users'] }); - } catch (error) { - console.error('Error fetching organisations:', error.message); - throw error; - } - } } diff --git a/src/entities/organisation.entity.ts b/src/entities/organisation.entity.ts deleted file mode 100644 index 0a575e7ae..000000000 --- a/src/entities/organisation.entity.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'; -import { User } from './user.entity'; - -@Entity() -export class Organisation { - @PrimaryGeneratedColumn('uuid') - org_id: string; - - @Column({ nullable: false }) - org_name: string; - - @Column({ nullable: false }) - description: string; - - @ManyToMany(() => User, user => user.organisations) - users: User[]; -} diff --git a/src/entities/parent.entity.ts b/src/entities/parent.entity.ts new file mode 100644 index 000000000..123ed984e --- /dev/null +++ b/src/entities/parent.entity.ts @@ -0,0 +1,13 @@ +import { BeforeInsert, Column, Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity() +export class Parent { + @PrimaryGeneratedColumn('uuid') + id: string; + + @CreateDateColumn({ name: 'created_at' }) + created_at: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updated_at: Date; +} \ No newline at end of file diff --git a/src/entities/product.entity.ts b/src/entities/product.entity.ts deleted file mode 100644 index 81fdea9eb..000000000 --- a/src/entities/product.entity.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; -import { User } from './user.entity'; - -@Entity() -export class Product { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ nullable: false }) - product_name: string; - - @Column({ nullable: false }) - product_price: number; - - @Column({ nullable: false }) - description: string; - - @ManyToOne(() => User, user => user.products) - @JoinColumn({ name: 'user_id' }) - user: User; -} diff --git a/src/entities/profile.entity.ts b/src/entities/profile.entity.ts deleted file mode 100644 index 33c12429d..000000000 --- a/src/entities/profile.entity.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Column, Entity, PrimaryGeneratedColumn, OneToOne, PrimaryColumn } from 'typeorm'; -import { User } from './user.entity'; - -@Entity() -export class Profile { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ unique: true }) - username: string; - - @Column({ nullable: true }) - bio: string; - - @Column({ nullable: false }) - phone: string; - - @Column() - avatar_image: string; - - @OneToOne(() => User, user => user.profile) - user: User; -} diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts deleted file mode 100644 index b6c08a8e9..000000000 --- a/src/entities/user.entity.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { IsString } from 'class-validator'; -import { - BeforeInsert, - Column, - Entity, - JoinColumn, - JoinTable, - ManyToMany, - OneToMany, - OneToOne, - PrimaryGeneratedColumn, - CreateDateColumn, - UpdateDateColumn, -} from 'typeorm'; -import * as bcrypt from 'bcrypt'; -import { Product } from './product.entity'; -import { Organisation } from './organisation.entity'; -import { Profile } from './profile.entity'; - -@Entity() -export class User { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ nullable: false }) - first_name: string; - - @Column({ nullable: false }) - last_name: string; - - @Column({ unique: true, nullable: false }) - email: string; - - @Column({ nullable: false }) - password: string; - - @Column({ nullable: true }) - is_active: boolean; - - @Column({ nullable: true }) - attempts_left: number; - - @Column({ nullable: true }) - time_left: number; - - @CreateDateColumn() - created_at: Date; - - @UpdateDateColumn() - updated_at: Date; - - @OneToMany(() => Product, product => product.user, { cascade: true }) - products: Product[]; - - @ManyToMany(() => Organisation, organisation => organisation.users, { cascade: true }) - @JoinTable({ - joinColumn: { name: 'user_id', referencedColumnName: 'id' }, - inverseJoinColumn: { name: 'organisation_org_id', referencedColumnName: 'org_id' }, - }) - organisations: Organisation[]; - - @OneToOne(() => Profile, profile => profile.user, { cascade: true }) - @JoinColumn({ name: 'profile_id' }) - profile: Profile; - - @BeforeInsert() - async hashPassword() { - this.password = await bcrypt.hash(this.password, 10); - } -} diff --git a/src/helpers/SystemMessages.ts b/src/helpers/SystemMessages.ts new file mode 100644 index 000000000..967091f3b --- /dev/null +++ b/src/helpers/SystemMessages.ts @@ -0,0 +1,5 @@ +export const USER_CREATED_SUCCESSFULLY = 'User Created Successfully'; +export const FAILED_TO_CREATE_USER = 'Error Occured while creating user, kindly try again'; +export const ERROR_OCCURED = 'Error Occured Performing this request'; +export const USER_ACCOUNT_EXIST = "Account with the specified email exists" +export const UNAUTHENTICATED_MESSAGE = "User is currently unauthorized, kindly authenticate to continue" diff --git a/src/helpers/skipAuth.ts b/src/helpers/skipAuth.ts new file mode 100644 index 000000000..6456eea81 --- /dev/null +++ b/src/helpers/skipAuth.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const skipAuth = () => SetMetadata(IS_PUBLIC_KEY, true); \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 1a536dbb8..77596ba8a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,7 +29,7 @@ async function bootstrap() { app.enable('trust proxy'); app.useLogger(logger); app.enableCors(); - app.setGlobalPrefix('api/v1'); + app.setGlobalPrefix('api/v1', { exclude: ["/", "health"] }); // TODO: set options for swagger docs const options = new DocumentBuilder() diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 000000000..880d50d19 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,19 @@ +import { Body, Controller, HttpStatus, Post, Request, Res } from '@nestjs/common'; +import { Response } from 'express'; +import { CreateUserDTO } from './dto/create-user.dto'; +import { skipAuth } from '../../helpers/skipAuth'; +import AuthenticationService from './auth.service'; + +@Controller('auth') +export default class RegistrationController { + constructor( + private authService: AuthenticationService, + ) { } + + @skipAuth() + @Post("register") + public async register(@Body() body: CreateUserDTO, @Res() response: Response): Promise { + const createUserResponse = await this.authService.createNewUser(body) + return response.status(createUserResponse.status_code).send(createUserResponse) + } +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 000000000..200c202df --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import RegistrationController from './auth.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '../user/entities/user.entity'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { appConfig } from '../../../config/appConfig'; +import { Repository } from 'typeorm'; +import AuthenticationService from './auth.service'; +import UserService from '../user/user.service'; + + +@Module({ + controllers: [RegistrationController], + providers: [AuthenticationService, Repository, UserService], + imports: [ + TypeOrmModule.forFeature([User]), + PassportModule, + JwtModule.register({ + global: true, + secret: appConfig.jwt.jwtSecret, + signOptions: { expiresIn: `${appConfig.jwt.jwtExpiry}s` }, + }), + ], +}) +export class AuthModule { } diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 000000000..856efacfd --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,69 @@ +import { HttpStatus, Injectable } from '@nestjs/common'; +import { CreateUserDTO } from './dto/create-user.dto'; +import { ERROR_OCCURED, FAILED_TO_CREATE_USER, USER_ACCOUNT_EXIST, USER_CREATED_SUCCESSFULLY } from '../../helpers/SystemMessages'; +import { JwtService } from '@nestjs/jwt'; +import UserService from '../user/user.service'; + +@Injectable() +export default class AuthenticationService { + constructor( + private userService: UserService, + private jwtService: JwtService + ) { } + + async createNewUser(creatUserDto: CreateUserDTO) { + try { + const userExists = await this.userService.getUserRecord({ identifier: creatUserDto.email, identifierType: "email" }) + + if (userExists) { + return { + status_code: HttpStatus.BAD_REQUEST, + message: USER_ACCOUNT_EXIST + } + } + + await this.userService.createUser(creatUserDto); + + const user = await this.userService.getUserRecord({ identifier: creatUserDto.email, identifierType: "email" }) + + if (!user) { + return { + status_code: HttpStatus.BAD_REQUEST, + message: FAILED_TO_CREATE_USER, + } + } + + const accessToken = this.jwtService.sign({ + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + sub: user.id + }); + + const responsePayload = { + token: accessToken, + user: { + + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + created_at: user.created_at, + }, + }; + + return { + status_code: HttpStatus.CREATED, + message: USER_CREATED_SUCCESSFULLY, + data: responsePayload, + } + + } catch (createNewUserError) { + console.log('AuthenticationServiceError ~ createNewUserError ~', createNewUserError); + return { + message: ERROR_OCCURED, + status_code: HttpStatus.INTERNAL_SERVER_ERROR, + } + } + } + +} diff --git a/src/modules/auth/dto/create-user.dto.ts b/src/modules/auth/dto/create-user.dto.ts new file mode 100644 index 000000000..d60508f15 --- /dev/null +++ b/src/modules/auth/dto/create-user.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; + +export class CreateUserDTO { + + @IsEmail() + email: string; + + + @IsNotEmpty() + first_name: string; + + + @IsNotEmpty() + last_name: string; + + + @IsNotEmpty() + @MinLength(8) + password: string; +} diff --git a/src/modules/auth/dto/user-resonse.dto.ts b/src/modules/auth/dto/user-resonse.dto.ts new file mode 100644 index 000000000..7d464be00 --- /dev/null +++ b/src/modules/auth/dto/user-resonse.dto.ts @@ -0,0 +1,4 @@ +import UserInterface from "../../user/interfaces/UserInterface"; + +type UserResponseDTO = Pick +export default UserResponseDTO \ No newline at end of file diff --git a/src/modules/auth/tests/auth.service.spec.ts b/src/modules/auth/tests/auth.service.spec.ts new file mode 100644 index 000000000..fba844321 --- /dev/null +++ b/src/modules/auth/tests/auth.service.spec.ts @@ -0,0 +1,109 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { JwtService } from "@nestjs/jwt"; +import UserService from "../../user/user.service" +import { User } from "../../user/entities/user.entity"; +import { getRepositoryToken } from "@nestjs/typeorm"; +import { ERROR_OCCURED, USER_ACCOUNT_EXIST, USER_CREATED_SUCCESSFULLY } from '../../../helpers/SystemMessages'; +import { CreateUserDTO } from '../dto/create-user.dto'; +import { HttpStatus } from '@nestjs/common'; +import UserResponseDTO from "../dto/user-resonse.dto"; +import AuthenticationService from "../auth.service"; + +describe("Authentication Service tests", () => { + + let userService: UserService + let authService: AuthenticationService + let jwtService: JwtService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [JwtService, AuthenticationService, UserService, { + provide: getRepositoryToken(User), + useValue: {} + }], + }).compile() + + userService = module.get(UserService) + authService = module.get(AuthenticationService) + jwtService = module.get(JwtService) + }) + + it("Registration Controller should be defined", () => { + expect(authService).toBeDefined() + }) + + it("should return BAD_REQUEST if user already exists", async () => { + const body: CreateUserDTO = { email: 'test@example.com', first_name: 'John', last_name: 'Doe', password: 'password' }; + const existingRecord: UserResponseDTO = { + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + is_active: true, + id: "some-uuid-value-here", + attempts_left: 2, + created_at: new Date(), + updated_at: new Date(), + } + jest.spyOn(userService, 'getUserRecord').mockResolvedValueOnce(existingRecord); + const newUserResponse = await authService.createNewUser(body) + expect(newUserResponse).toEqual({ + status_code: HttpStatus.BAD_REQUEST, + message: USER_ACCOUNT_EXIST, + }); + }) + + it('should return CREATED and user data if registration is successful', async () => { + const body: CreateUserDTO = { email: 'test@example.com', first_name: 'John', last_name: 'Doe', password: 'password' }; + const user: UserResponseDTO = { + id: "1", + email: body.email, + first_name: body.first_name, + last_name: body.last_name, + attempts_left: 2, + is_active: true, + created_at: new Date(), + updated_at: new Date(), + }; + const accessToken = 'fake-jwt-token'; + + + jest.spyOn(userService, 'getUserRecord').mockResolvedValueOnce(null); + jest.spyOn(userService, 'getUserRecord').mockResolvedValueOnce(user); + jest.spyOn(jwtService, 'sign').mockReturnValue(accessToken); + jest.spyOn(userService, 'createUser').mockResolvedValueOnce(null); + + const newUserResponse = await authService.createNewUser(body) + + user.created_at = newUserResponse.data.user.created_at + + expect(newUserResponse).toEqual({ + status_code: HttpStatus.CREATED, + message: USER_CREATED_SUCCESSFULLY, + data: { + token: accessToken, + user: { + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + created_at: user.created_at, + }, + }, + }); + }) + + + it('should return INTERNAL_SERVER_ERROR on exception', async () => { + const body: CreateUserDTO = { email: 'john@doe.com', first_name: 'John', last_name: 'Doe', password: 'password' }; + + jest.spyOn(userService, 'getUserRecord').mockImplementationOnce(() => { + throw new Error('Test error'); + }); + + const newUserResponse = await authService.createNewUser(body) + + expect(newUserResponse).toEqual({ + message: ERROR_OCCURED, + status_code: HttpStatus.INTERNAL_SERVER_ERROR, + }); + }); +}) diff --git a/src/modules/user/dto/user-response.dto.ts b/src/modules/user/dto/user-response.dto.ts new file mode 100644 index 000000000..a3bc517ea --- /dev/null +++ b/src/modules/user/dto/user-response.dto.ts @@ -0,0 +1,4 @@ +import UserInterface from "../interfaces/UserInterface"; + +type UserResponseDTO = Pick +export default UserResponseDTO \ No newline at end of file diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts new file mode 100644 index 000000000..9f758fe51 --- /dev/null +++ b/src/modules/user/entities/user.entity.ts @@ -0,0 +1,40 @@ +import { + BeforeInsert, + Column, + Entity, +} from 'typeorm'; +import * as bcrypt from 'bcrypt'; +import { Parent } from '../../../entities/parent.entity'; + + +@Entity() +export class User extends Parent { + + @Column({ nullable: false }) + first_name: string; + + @Column({ nullable: false }) + last_name: string; + + @Column({ unique: true, nullable: false }) + email: string; + + @Column({ nullable: false }) + password: string; + + @Column({ nullable: true }) + is_active: boolean; + + @Column({ nullable: true }) + attempts_left: number; + + @Column({ nullable: true }) + time_left: number; + + + @BeforeInsert() + async hashPassword() { + this.password = await bcrypt.hash(this.password, 10); + } +} + diff --git a/src/modules/user/interfaces/UserInterface.ts b/src/modules/user/interfaces/UserInterface.ts new file mode 100644 index 000000000..1f087a34b --- /dev/null +++ b/src/modules/user/interfaces/UserInterface.ts @@ -0,0 +1,24 @@ +interface UserInterface { + id: string; + + email: string; + + first_name: string; + + last_name: string; + + password: string; + + is_active: boolean + + attempts_left: number + + time_left: number + + created_at: Date; + + updated_at: Date; + +} + +export default UserInterface diff --git a/src/modules/user/options/CreateNewUserOptions.ts b/src/modules/user/options/CreateNewUserOptions.ts new file mode 100644 index 000000000..1e02d2916 --- /dev/null +++ b/src/modules/user/options/CreateNewUserOptions.ts @@ -0,0 +1,4 @@ +import UserInterface from "../../user/interfaces/UserInterface"; + +type CreateNewUserOptions = Pick; +export default CreateNewUserOptions; \ No newline at end of file diff --git a/src/modules/user/options/UserIdentifierOptions.ts b/src/modules/user/options/UserIdentifierOptions.ts new file mode 100644 index 000000000..94566858d --- /dev/null +++ b/src/modules/user/options/UserIdentifierOptions.ts @@ -0,0 +1,10 @@ +type UserIdentifierOptions = { + identifierType: "id", + identifier: string +} | + { + identifierType: "email", + identifier: string +} + +export default UserIdentifierOptions \ No newline at end of file diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts new file mode 100644 index 000000000..cc017c151 --- /dev/null +++ b/src/modules/user/user.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '../user/entities/user.entity'; +import { Repository } from 'typeorm'; +import UserService from './user.service'; + + + +@Module({ + controllers: [], + + providers: [UserService, Repository], + imports: [ + TypeOrmModule.forFeature([User]), + ], + exports: [UserService], +}) +export class UserModule { } diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts new file mode 100644 index 000000000..2cccac903 --- /dev/null +++ b/src/modules/user/user.service.ts @@ -0,0 +1,44 @@ +import { Repository } from 'typeorm'; +import { User } from './entities/user.entity'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import CreateNewUserOptions from './options/CreateNewUserOptions'; +import UserIdentifierOptions from './options/UserIdentifierOptions'; +import UserResponseDTO from './dto/user-response.dto'; + + +@Injectable() +export default class UserService { + constructor( + @InjectRepository(User) + private userRepository: Repository) { } + + async createUser(user: CreateNewUserOptions) { + + const newUser = new User() + Object.assign(newUser, user) + await this.userRepository.save(newUser) + } + + private async getUserByEmail(email: string) { + const user: UserResponseDTO = await this.userRepository.findOne({ where: { email: email } }); + return user + } + + private async getUserById(identifier: string) { + const user: UserResponseDTO = await this.userRepository.findOne({ where: { id: identifier } }); + return user + } + + async getUserRecord(identifierOptions: UserIdentifierOptions) { + const { identifier, identifierType } = identifierOptions + + const GetRecord = { + id: async () => this.getUserById(String(identifier)), + email: async () => this.getUserByEmail(String(identifier)) + } + + return await GetRecord[identifierType]() + } + +}