Skip to content

Commit d029ed8

Browse files
feat(auth): implement login and signup use case and add e2e tests
1 parent 298ceca commit d029ed8

26 files changed

+426
-34
lines changed

.env.test

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
DATABASE_NAME=db-test-e2e
2+
DATABASE_HOST=127.0.0.1
3+
DATABASE_PORT=25432
4+
DATABASE_USER=postgres
5+
DATABASE_PASSWORD=postgres
6+
17
JWT_SECRET=nestjs-ddd-devops
28
JWT_REFRESH_SECRET=nestjs-ddd-devops-refresh
39
JWT_EXPIRES_IN=3600
410
JWT_REFRESH_EXPIRES_IN=360000
11+
12+
PORT=3000

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ web_modules/
7474

7575
# dotenv environment variable files
7676
.env
77-
.env.test
7877
.env.development.local
7978
.env.test.local
8079
.env.production.local

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ In the following chapters you will find a description of the main choices, techn
5353
```bash
5454
docker-compose up -d
5555
```
56-
4. Provide a ```.env``` file with all required environment variables _(check out ```.env.dist``` example file)_
56+
4. Provide a ```.env``` and ```.env.test``` files with all required environment variables _(check out ```.env.dist``` example file)_
5757
5. Create and generate the database schema from your entities' metadata:
5858
```bash
5959
npm run schema:update

docker-compose.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ services:
77
ports:
88
- "15432:5432/tcp"
99
environment:
10-
- POSTGRES_DB=db-test
10+
- POSTGRES_DB=db-dev
1111
- POSTGRES_USER=postgres
1212
- POSTGRES_PASSWORD=postgres
1313
volumes:
@@ -17,6 +17,21 @@ services:
1717
networks:
1818
- postgres
1919

20+
postgresql-test-e2e:
21+
container_name: postgresql-test-e2e
22+
image: postgres:latest
23+
restart: unless-stopped
24+
ports:
25+
- "25432:5432/tcp"
26+
environment:
27+
- POSTGRES_DB=db-test-e2e
28+
- POSTGRES_USER=postgres
29+
- POSTGRES_PASSWORD=postgres
30+
stdin_open: true
31+
tty: true
32+
networks:
33+
- postgres
34+
2035
volumes:
2136
postgresql_volume:
2237

src/libs/api/api-role.enum.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum ApiRole {
2+
ADMIN = 0,
3+
USER = 1,
4+
}

src/modules/auth/api/controller/auth.controller.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,26 @@ import {
88
import { LoginBody } from '../presentation/body/login.body';
99
import { SignupBody } from '../presentation/body/signup.body';
1010
import { PublicApi } from '../../../../libs/decorator/auth.decorator';
11-
import { CommandBus } from '@nestjs/cqrs';
1211
import { getOrThrowWith, Option } from 'effect/Option';
1312
import { JwtUser } from '../presentation/dto/jwt-user.dto';
1413
import { JwtAuthService } from '../../application/service/jwt-auth-service.interface';
15-
import { JWT_AUTHENTICATION, LOGIN_USE_CASE } from '../../auth.tokens';
14+
import {
15+
JWT_AUTH_SERVICE,
16+
LOGIN_USE_CASE,
17+
SIGNUP_USE_CASE,
18+
} from '../../auth.tokens';
1619
import { UseCase } from '../../../../libs/ddd/use-case.interface';
1720
import { AuthUser } from '../presentation/dto/auth-user.dto';
18-
import { SignupCommand } from '../../application/command/signup.command';
1921

2022
@Controller('auth')
2123
export class AuthController {
2224
constructor(
23-
private readonly commandBus: CommandBus,
24-
@Inject(JWT_AUTHENTICATION)
25+
@Inject(JWT_AUTH_SERVICE)
2526
private readonly jwtAuth: JwtAuthService,
2627
@Inject(LOGIN_USE_CASE)
2728
private readonly loginUseCase: UseCase<LoginBody, Option<AuthUser>>,
29+
@Inject(SIGNUP_USE_CASE)
30+
private readonly signupUseCase: UseCase<SignupBody, Option<AuthUser>>,
2831
) {}
2932

3033
@PublicApi()
@@ -41,12 +44,10 @@ export class AuthController {
4144
@PublicApi()
4245
@Post('/signup')
4346
async signup(@Body() body: SignupBody) {
44-
return await this.commandBus.execute(
45-
new SignupCommand(
46-
body.email,
47-
body.password,
48-
body.firstName,
49-
body.lastName,
47+
return this.jwtAuth.generateJwtUser(
48+
getOrThrowWith(
49+
await this.signupUseCase.execute(body),
50+
() => new UnauthorizedException('Signup Error!'),
5051
),
5152
);
5253
}
Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,70 @@
1-
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
1+
import {
2+
CanActivate,
3+
ExecutionContext,
4+
Inject,
5+
Injectable,
6+
} from '@nestjs/common';
7+
import { FastifyRequest } from 'fastify';
8+
import { JwtService } from '../../infrastructure/jwt/jwt.service';
9+
import { Reflector } from '@nestjs/core';
10+
import { AUTH_ROLES_KEY, JWT_AUTH_SERVICE } from '../../auth.tokens';
11+
import { isNone, none, Option, some } from 'effect/Option';
12+
import { QueryBus } from '@nestjs/cqrs';
13+
import { CheckAuthUserByIdQuery } from '../../application/query/check-auth-user-by-id.query';
14+
import { IS_PUBLIC_API } from '../../../../libs/decorator/auth.decorator';
15+
import { ApiRole } from '../../../../libs/api/api-role.enum';
216

317
@Injectable()
418
export class AuthGuard implements CanActivate {
5-
constructor() {}
19+
private authenticationHeaders: string[] = ['Authorization', 'authorization'];
20+
21+
constructor(
22+
private readonly reflector: Reflector,
23+
@Inject(JWT_AUTH_SERVICE)
24+
private readonly jwtService: JwtService,
25+
private readonly queryBus: QueryBus,
26+
) {}
627

728
async canActivate(context: ExecutionContext): Promise<boolean> {
8-
console.log(context);
9-
return true;
29+
const apiRoles = this.reflector.getAllAndOverride<ApiRole[]>(
30+
AUTH_ROLES_KEY,
31+
[context.getHandler(), context.getClass()],
32+
);
33+
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_API, [
34+
context.getHandler(),
35+
context.getClass(),
36+
]);
37+
if (isPublic && !apiRoles) return true;
38+
const request = context.switchToHttp().getRequest<FastifyRequest>();
39+
const token = this.extractToken(request);
40+
if (isNone(token)) return false;
41+
try {
42+
const authUser = await this.jwtService.verifyToken(token.value);
43+
if (isNone(authUser)) return false;
44+
request['user'] = authUser.value;
45+
const isActiveUser = await this.isActiveUser(authUser.value.id);
46+
return isActiveUser && apiRoles.includes(authUser.value.role);
47+
} catch {
48+
return false;
49+
}
50+
}
51+
52+
private extractToken(request: FastifyRequest): Option<string> {
53+
for (const header of this.authenticationHeaders) {
54+
const tokenHeader = request.headers[header] as string;
55+
if (tokenHeader) {
56+
const splitted = tokenHeader.split(' ');
57+
if (splitted[0] !== 'Bearer') {
58+
return none();
59+
} else {
60+
return some(splitted[1]);
61+
}
62+
}
63+
}
64+
return none();
65+
}
66+
67+
private async isActiveUser(userId: string): Promise<boolean> {
68+
return await this.queryBus.execute(new CheckAuthUserByIdQuery(userId));
1069
}
1170
}

src/modules/auth/application/command/create-user.command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ICommand } from '@nestjs/cqrs';
22

3-
export class SignupCommand implements ICommand {
3+
export class CreateUserCommand implements ICommand {
44
constructor(
55
readonly email: string,
66
readonly password: string,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export class CheckAuthUserByIdQuery {
2+
constructor(readonly id: string) {}
3+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export class CheckAuthUserByEmailQuery {
1+
export class GetAuthUserByEmailQuery {
22
constructor(readonly email: string) {}
33
}

0 commit comments

Comments
 (0)