Skip to content

Commit

Permalink
Fix login, confirm code
Browse files Browse the repository at this point in the history
  • Loading branch information
PhamAnhHoang committed Dec 16, 2023
1 parent 2c169bb commit 9cf3870
Show file tree
Hide file tree
Showing 17 changed files with 319 additions and 16 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
# API
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:4200
PORT=3000

JWT_ACCESS_TOKEN_EXPIRATION_TIME=60
JWT_REFRESH_TOKEN_EXPIRATION_TIME=28800
4 changes: 1 addition & 3 deletions apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ export class AuthController {
async confirmCode(
@Body() body: Pure<ConfirmationCodeDto>
): Promise<UserEntity> {
const user = await this.confirmCodeService.verifyCode(body.code, body.email)

await this.authService.sendGreetings(user)
const user = await this.authService.verifyCode(body)

return user
}
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { JwtStrategy } from './strategies/jwt.strategy'
import { LocalStrategy } from './strategies/local.strategy'
import { MailerModule } from '../mailer/mailer.module'
import { ConfirmCodeModule } from '../user/confirm-code.module'
import { OrganizationModule } from '../organization/organization.module'

@Module({
imports: [
Expand All @@ -26,7 +27,8 @@ import { ConfirmCodeModule } from '../user/confirm-code.module'
algorithms: ['HS384']
}
}),
ConfirmCodeModule
ConfirmCodeModule,
OrganizationModule
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy, SessionSerializer]
Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,9 @@ describe('AuthService', () => {
const user = createMock<UserEntity>({ email: '[email protected]' })

mockedJwtService.sign.mockReturnValueOnce('j.w.t')
const token = service.signToken(user)
const {refresh_token, access_token} = service.signToken(user)

expect(token).toEqual(expect.any(String))
expect(access_token).toEqual(expect.any(String))
expect(refresh_token).toEqual(expect.any(String))
})
})
51 changes: 47 additions & 4 deletions apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { JwtService } from '@nestjs/jwt'

import { UserEntity } from '../entities/user.entity'
import {
ConfirmationCodeDto,
ResetPasswordRequestDto,
SignUpWithEmailCredentialsDto
} from '@isomera/dtos'
Expand All @@ -17,14 +18,18 @@ import { MailerService } from '../mailer/mailer.service'
import { ConfirmCodeService } from '../user/confirm-code.service'
import { Pure } from '@isomera/interfaces'
import { generateRandomStringUtil } from '@isomera/utils'
import { OrganizationService } from '../organization/organization.service'
import { ConfigService } from '@nestjs/config'

@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
private readonly mailerService: MailerService,
private readonly confirmCode: ConfirmCodeService
private readonly confirmCode: ConfirmCodeService,
private readonly organizationService: OrganizationService,
private readonly configService: ConfigService
) {}

async register(
Expand Down Expand Up @@ -93,12 +98,35 @@ export class AuthService {
return user
}

signToken(user: UserEntity): string {
signToken(user: UserEntity): {refresh_token: string, access_token: string} {
return {
refresh_token: this.generateRefreshToken(user.email),
access_token: this.generateAccessToken(user.email)
}
}

private generateAccessToken(email: string): string {
const payload = {
sub: user.email
sub: email
}

return this.jwtService.sign(payload)
return this.jwtService.sign(payload, {
expiresIn: `${this.configService.get<string>(
'JWT_ACCESS_TOKEN_EXPIRATION_TIME',
)}s`,
});
}

private generateRefreshToken(email: string): string {
const payload = {
sub: email
}

return this.jwtService.sign(payload, {
expiresIn: `${this.configService.get<string>(
'JWT_REFRESH_TOKEN_EXPIRATION_TIME',
)}s`,
});
}

async sendGreetings(user: UserEntity) {
Expand Down Expand Up @@ -146,4 +174,19 @@ export class AuthService {
}
return false
}

/**
* After verify user, create personal organization for this user and send email
* @param code
* @param email
*/
public async verifyCode({code, email}: Pure<ConfirmationCodeDto>): Promise<UserEntity> {
const user = await this.confirmCode.verifyCode(code, email);

await this.organizationService.createDefaultOrganization(user.id);

await this.sendGreetings(user);

return user
}
}
7 changes: 6 additions & 1 deletion apps/api/src/auth/interceptors/token.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ describe('TokenInterceptor', () => {

jest
.spyOn(mockedAuthService, 'signToken')
.mockImplementationOnce(() => 'jwt')
.mockImplementationOnce(() => {
return {
refresh_token: 'refresh_token',
access_token: 'jwt'
}
})
jest.spyOn(res, 'getHeader').mockReturnValue('Bearer j.w.t')

expect(res.getHeader('Authorization')).toBe('Bearer j.w.t')
Expand Down
10 changes: 5 additions & 5 deletions apps/api/src/auth/interceptors/token.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,21 @@ export class TokenInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<UserEntity>
): Observable<UserEntity> {
): Observable<Partial<UserEntity> & {access_token: string, refresh_token: string}> {
return next.handle().pipe(
map(user => {
const response = context.switchToHttp().getResponse<Response>()
const token = this.authService.signToken(user)
const {refresh_token, access_token} = this.authService.signToken(user)

response.setHeader('Authorization', `Bearer ${token}`)
response.cookie('token', token, {
response.setHeader('Authorization', `Bearer ${access_token}`)
response.cookie('token', access_token, {
httpOnly: true,
signed: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production'
})

return user
return {...user, access_token, refresh_token}
})
)
}
Expand Down
25 changes: 25 additions & 0 deletions apps/api/src/entities/organization.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm'
import { OrganizationInterface } from '@isomera/interfaces'

@Entity({ name: 'organizations' })
export class OrganizationEntity implements OrganizationInterface {
@PrimaryGeneratedColumn()
id: number

@Column({ nullable: false })
name: string

@CreateDateColumn()
createdAt: Date

@UpdateDateColumn()
updatedAt: Date

static DEFAULT_ORGANIZATION_NAME = 'Isomera personal user'
}
25 changes: 25 additions & 0 deletions apps/api/src/entities/user-organization.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
} from 'typeorm'
import { UserOrganizationInterace } from '@isomera/interfaces'

@Entity({ name: 'user-organization' })
export class UserOrganizationEntity implements UserOrganizationInterace {
@PrimaryGeneratedColumn()
id: number

@Column({ nullable: false })
userId: number

@Column({ nullable: false })
organizationId: number

@Column({ nullable: false })
role: number

@CreateDateColumn()
createdAt: Date
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner, Table, TableColumn } from "typeorm"

export class CreateOrganizationsTable1702703853544 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'organizations',
columns: [
new TableColumn({
name: 'id',
type: 'int',
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment'
}),
new TableColumn({
name: 'name',
type: 'varchar(255)',
isNullable: false
}),
new TableColumn({
name: 'createdAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
isNullable: false
}),
new TableColumn({
name: 'updatedAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP()',
isNullable: false
})
]
}),
true
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('organizations')
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { MigrationInterface, QueryRunner, Table, TableColumn } from "typeorm"

export class CreateUserOrganizationTable1702703944825 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'user-organization',
columns: [
new TableColumn({
name: 'id',
type: 'int',
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment'
}),
new TableColumn({
name: 'userId',
type: 'int',
isNullable: false
}),
new TableColumn({
name: 'organizationId',
type: 'int',
isNullable: false
}),
new TableColumn({
name: 'role',
type: 'int',
isNullable: false
}),
new TableColumn({
name: 'createdAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
isNullable: false
}),
new TableColumn({
name: 'updatedAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP()',
isNullable: false
})
]
}),
true
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('user-organization')
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { MigrationInterface, QueryRunner, TableForeignKey } from "typeorm"

export class AddForeignKeyUserOrganizationTable1702704151500 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createForeignKey(
'user-organization',
new TableForeignKey({
name: 'FK_users_of_user-organization',
columnNames: ['userId'],
referencedColumnNames: ['id'],
referencedTableName: 'users',
onDelete: 'CASCADE'
})
)

await queryRunner.createForeignKey(
'user-organization',
new TableForeignKey({
name: 'FK_organizations_of_user-organization',
columnNames: ['organizationId'],
referencedColumnNames: ['id'],
referencedTableName: 'organizations',
onDelete: 'CASCADE'
})
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable('user-organization')
const foreignKeyUser = table.foreignKeys.find(
fk => fk.columnNames.indexOf('userId') !== -1
)
await queryRunner.dropForeignKey('user_organization', foreignKeyUser)

const foreignKeyOrganization = table.foreignKeys.find(
fk => fk.columnNames.indexOf('organizationId') !== -1
)
await queryRunner.dropForeignKey('user_organization', foreignKeyOrganization)
}
}
12 changes: 12 additions & 0 deletions apps/api/src/organization/organization.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { OrganizationService } from './organization.service'
import { OrganizationEntity } from '../entities/organization.entity'
import { UserOrganizationEntity } from '../entities/user-organization.entity'

@Module({
imports: [TypeOrmModule.forFeature([OrganizationEntity, UserOrganizationEntity]), OrganizationEntity, UserOrganizationEntity],
providers: [OrganizationService],
exports: [OrganizationService]
})
export class OrganizationModule {}
Loading

0 comments on commit 9cf3870

Please sign in to comment.