Skip to content

Commit

Permalink
emails entity
Browse files Browse the repository at this point in the history
  • Loading branch information
mrkeksz committed Dec 17, 2023
1 parent ed378b0 commit 62227be
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 73 deletions.
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {graphqlModuleOptions} from './graphql-module.options'
import {ConfigModule} from './config/config.module'
import {EmailVerificationModule} from './email-verification/email-verification.module'
import {RefreshTokenModule} from './refresh-token/refresh-token.module'
import {EmailsModule} from './emails/emails.module'

@Module({
imports: [
Expand All @@ -27,6 +28,7 @@ import {RefreshTokenModule} from './refresh-token/refresh-token.module'
GraphQLModule.forRootAsync(graphqlModuleOptions),
EmailVerificationModule,
RefreshTokenModule,
EmailsModule,
],
providers: [
ComplexityPlugin,
Expand Down
18 changes: 0 additions & 18 deletions src/email-verification/email-verification.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,5 @@ describe('EmailVerificationService', () => {
).resolves.toBeDefined()

expect(verificationCode.status).toBe('used')
expect(usersService.activateUserById).not.toHaveBeenCalled()
})

it('should activate user when verifying email', async () => {
const user = new User()
user.status = 'unconfirmed'
const verificationCode = new EmailVerificationCode()
verificationCode.code = 123456

jest.spyOn(usersService, 'getUserByEmail').mockResolvedValue(user)
jest.spyOn(verificationCodeRepository, 'findOne').mockResolvedValue(verificationCode)
jest.spyOn(usersService, 'activateUserById').mockResolvedValue(undefined as any)
jest.spyOn(tokenService, 'generateAndSaveTokens').mockResolvedValue(new Tokens())
jest.spyOn(verificationCodeRepository, 'save').mockResolvedValue(undefined as any)

await service.verifyEmail('[email protected]', 123456, 'deviceInfo')

expect(usersService.activateUserById).toHaveBeenCalledWith(user.id)
})
})
5 changes: 0 additions & 5 deletions src/email-verification/email-verification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,6 @@ export class EmailVerificationService {
throw new UnauthorizedException('Invalid verification code')
}

if (user.status === 'unconfirmed') {
this.logger.debug(`Activating user "${user.id}"`)
await this.usersService.activateUserById(user.id)
}

verificationCode.status = 'used'
await this.verificationCodeRepository.save(verificationCode)
return this.tokenService.generateAndSaveTokens(user, deviceInfo)
Expand Down
8 changes: 8 additions & 0 deletions src/emails/emails.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {Module} from '@nestjs/common'
import {TypeOrmModule} from '@nestjs/typeorm'
import {Email} from './entities/email.entity'

@Module({
imports: [TypeOrmModule.forFeature([Email])],
})
export class EmailsModule {}
32 changes: 32 additions & 0 deletions src/emails/entities/email.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
OneToOne,
JoinColumn,
} from 'typeorm'
import {User} from '../../users/entities/user.entity'

@Entity()
export class Email {
@PrimaryGeneratedColumn('uuid')
id: string

@Column({unique: true})
email: string

// Email does not have a status field because it is not necessary to deactivate/activate it
// and email notifications will be realized by another service and another entity

@OneToOne(() => User, {onDelete: 'CASCADE', nullable: false})
@JoinColumn()
user: User

@CreateDateColumn({type: 'timestamp with time zone'})
createdAt: Date

@UpdateDateColumn({type: 'timestamp with time zone'})
updatedAt: Date
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {getRepositoryToken} from '@nestjs/typeorm'
import {User} from '../../users/entities/user.entity'
import {ConfigModule} from '../../config/config.module'
import {UnauthorizedException} from '@nestjs/common'
import {Email} from '../../emails/entities/email.entity'

describe(JwtAccessTokenStrategy.name, () => {
let jwtStrategy: JwtAccessTokenStrategy
Expand Down Expand Up @@ -37,7 +38,9 @@ describe(JwtAccessTokenStrategy.name, () => {
it('should return a user when user exists', async () => {
const expected = new User()
expected.id = 'test-id'
expected.email = 'test-email'
const email = new Email()
email.email = 'test-email'
expected.email = email

jest.spyOn(usersService, 'getUserById').mockResolvedValue(expected)

Expand Down
23 changes: 12 additions & 11 deletions src/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import {Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn} from 'typeorm'
import {
CreateDateColumn,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm'
import {Email} from '../../emails/entities/email.entity'

@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string

@Column({unique: true})
email: string

@Column({
comment: 'Status of the user',
type: 'enum',
enum: ['active', 'unconfirmed'],
default: 'unconfirmed',
})
status: 'active' | 'unconfirmed'
@OneToOne(() => Email, {nullable: true})
@JoinColumn()
email: Email | null

@CreateDateColumn({type: 'timestamp with time zone'})
createdAt: Date
Expand Down
29 changes: 12 additions & 17 deletions src/users/users.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {User} from './entities/user.entity'
import {UsersService} from './users.service'
import {Repository} from 'typeorm'
import {User} from './entities/user.entity'
import {Test, TestingModule} from '@nestjs/testing'
import {getRepositoryToken} from '@nestjs/typeorm'
import {Email} from '../emails/entities/email.entity'

describe('UsersService', () => {
let service: UsersService
Expand All @@ -25,7 +26,9 @@ describe('UsersService', () => {

it('should create a user by email', async () => {
const testUser = new User()
testUser.email = '[email protected]'
const email = new Email()
email.email = '[email protected]'
testUser.email = email

jest.spyOn(repo, 'create').mockReturnValueOnce(testUser)
jest.spyOn(repo, 'save').mockResolvedValueOnce(testUser)
Expand All @@ -36,11 +39,13 @@ describe('UsersService', () => {

it('should not create a user by email if it already exists', async () => {
const testUser = new User()
testUser.email = '[email protected]'
const email = new Email()
email.email = '[email protected]'
testUser.email = email
jest.spyOn(repo, 'findOne').mockResolvedValueOnce(testUser)
jest.spyOn(repo, 'create')
jest.spyOn(repo, 'save')
expect(await service.getOrCreateUserByEmail(testUser.email)).toEqual(testUser)
expect(await service.getOrCreateUserByEmail(testUser.email.email)).toEqual(testUser)
expect(repo.create).not.toHaveBeenCalled()
expect(repo.save).not.toHaveBeenCalled()
})
Expand All @@ -56,22 +61,12 @@ describe('UsersService', () => {

it('should get a user by email', async () => {
const testUser = new User()
testUser.email = '[email protected]'
const email = new Email()
email.email = '[email protected]'
testUser.email = email

jest.spyOn(repo, 'findOne').mockResolvedValueOnce(testUser)

expect(await service.getUserByEmail('[email protected]')).toEqual(testUser)
})

it('should activate a user by id', async () => {
const testUser = new User()
testUser.id = '1'
testUser.status = 'unconfirmed'

jest.spyOn(repo, 'update').mockResolvedValueOnce(undefined as any)

await service.activateUserById('1')

expect(repo.update).toHaveBeenCalledWith({id: '1'}, {status: 'active'})
})
})
26 changes: 5 additions & 21 deletions src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,19 @@ export class UsersService {
public async getOrCreateUserByEmail(email: string): Promise<User> {
const user = await this.getUserByEmail(email)
if (user) return user
return this.createUser({email})
return this.createUserByEmail(email)
}

public getUserById(id: string): Promise<User | null> {
return this.getUser('id', id)
return this.usersRepository.findOne({where: {id}})
}

public getUserByEmail(email: string): Promise<User | null> {
return this.getUser('email', email)
return this.usersRepository.findOne({where: {email: {email}}})
}

public async activateUserById(id: string): Promise<void> {
await this.updateUser('id', id, {status: 'active'})
}

private async createUser(data: Partial<User>): Promise<User> {
const newUser = this.usersRepository.create(data)
private async createUserByEmail(email: string): Promise<User> {
const newUser = this.usersRepository.create({email: {email}})
return this.usersRepository.save(newUser)
}

private async updateUser(
field: keyof User,
value: User[keyof User],
data: Partial<User>,
): Promise<void> {
await this.usersRepository.update({[field]: value}, data)
}

private getUser(field: keyof User, value: User[keyof User]): Promise<User | null> {
return this.usersRepository.findOne({where: {[field]: value}})
}
}

0 comments on commit 62227be

Please sign in to comment.