Skip to content

Commit

Permalink
create endpoint and update contract to activate user
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Sep 26, 2024
1 parent 3af4e45 commit 719eaca
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 9 deletions.
15 changes: 14 additions & 1 deletion api/src/modules/auth/authentication.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
UseInterceptors,
ClassSerializerInterceptor,
HttpStatus,
UnauthorizedException,
} from '@nestjs/common';
import { User } from '@shared/entities/users/user.entity';
import { LocalAuthGuard } from '@api/modules/auth/guards/local-auth.guard';
Expand All @@ -18,6 +17,8 @@ import { AuthGuard } from '@nestjs/passport';
import { ResetPassword } from '@api/modules/auth/strategies/reset-password.strategy';
import { authContract } from '@shared/contracts/auth.contract';
import { AuthenticationService } from '@api/modules/auth/authentication.service';
import { JwtAuthGuard } from '@api/modules/auth/guards/jwt-auth.guard';
import { SignUp } from '@api/modules/auth/strategies/sign-up.strategy';

@Controller()
@UseInterceptors(ClassSerializerInterceptor)
Expand All @@ -40,6 +41,18 @@ export class AuthenticationController {
});
}

@UseGuards(JwtAuthGuard, AuthGuard(SignUp))
@TsRestHandler(authContract.signUp)
async signUp(@GetUser() user: User): Promise<ControllerResponse> {
return tsRestHandler(authContract.login, async ({ body }) => {
await this.authService.signUp(user, body);
return {
body: null,
status: 201,
};
});
}

@UseGuards(AuthGuard(ResetPassword))
@TsRestHandler(authContract.resetPassword)
async resetPassword(@GetUser() user: User): Promise<ControllerResponse> {
Expand Down
15 changes: 14 additions & 1 deletion api/src/modules/auth/authentication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '@api/modules/users/users.service';
import { User } from '@shared/entities/users/user.entity';
import * as bcrypt from 'bcrypt';
import { CommandBus } from '@nestjs/cqrs';
import { CommandBus, EventBus } from '@nestjs/cqrs';
import { UserWithAccessToken } from '@shared/dtos/user.dto';
import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema';
import { CreateUserDto } from '@shared/schemas/users/create-user.schema';
import { randomBytes } from 'node:crypto';
import { SendWelcomeEmailCommand } from '@api/modules/notifications/email/commands/send-welcome-email.command';
import { JwtManager } from '@api/modules/auth/services/jwt.manager';
import { SignUpDto } from '@shared/schemas/auth/sign-up.schema';
import { UserSignedUpEvent } from '@api/modules/events/user-events/user-signed-up.event';

@Injectable()
export class AuthenticationService {
constructor(
private readonly usersService: UsersService,
private readonly jwtManager: JwtManager,
private readonly commandBus: CommandBus,
private readonly eventBus: EventBus,
) {}
async validateUser(email: string, password: string): Promise<User> {
const user = await this.usersService.findByEmail(email);
Expand Down Expand Up @@ -47,6 +50,16 @@ export class AuthenticationService {
return { user, accessToken };
}

async signUp(user: User, signUpDto: SignUpDto): Promise<void> {
const { password, newPassword } = signUpDto;
if (!(await bcrypt.compare(password, user.password))) {
throw new UnauthorizedException();
}
user.isActive = true;
await this.usersService.updatePassword(user, newPassword);
this.eventBus.publish(new UserSignedUpEvent(user.id, user.email));
}

async verifyToken(token: string, type: TOKEN_TYPE_ENUM): Promise<boolean> {
if (await this.jwtManager.isTokenValid(token, type)) {
return true;
Expand Down
4 changes: 2 additions & 2 deletions api/src/modules/auth/services/jwt.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class JwtManager {
): Promise<{ emailConfirmationToken: string; expiresIn: string }> {
const { token: emailConfirmationToken, expiresIn } = await this.sign(
userId,
TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION,
TOKEN_TYPE_ENUM.SIGN_UP,
);
return {
emailConfirmationToken,
Expand All @@ -71,7 +71,7 @@ export class JwtManager {
try {
const { id } = await this.jwt.verifyAsync(token, { secret });
switch (type) {
case TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION:
case TOKEN_TYPE_ENUM.SIGN_UP:
/**
* If the user is already active, we don't want to allow them to confirm their email again.
*/
Expand Down
33 changes: 33 additions & 0 deletions api/src/modules/auth/strategies/sign-up.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UsersService } from '@api/modules/users/users.service';
import { ApiConfigService } from '@api/modules/config/app-config.service';
import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema';

export type JwtPayload = { id: string };

export const SignUp = 'sign-up';

@Injectable()
export class SignUpStrategy extends PassportStrategy(Strategy, SignUp) {
constructor(
private readonly userService: UsersService,
private readonly config: ApiConfigService,
) {
const { secret } = config.getJWTConfigByType(TOKEN_TYPE_ENUM.SIGN_UP);
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: secret,
});
}

async validate(payload: JwtPayload) {
const { id } = payload;
const user = await this.userService.findOneBy(id);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
2 changes: 1 addition & 1 deletion api/src/modules/config/auth-config.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class JwtConfigHandler {
),
};

case TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION:
case TOKEN_TYPE_ENUM.SIGN_UP:
return {
secret: this.configService.get<string>(
'EMAIL_CONFIRMATION_TOKEN_SECRET',
Expand Down
2 changes: 1 addition & 1 deletion api/src/modules/events/events.enum.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export enum API_EVENT_TYPES {
USER_SIGNED_UP = 'user.signed_up',
USER_CREATED = 'user.created',
USER_PASSWORD_RECOVERY_REQUESTED = 'user.password_recovery_requested',
USER_PASSWORD_RECOVERY_REQUESTED_NON_EXISTENT = 'user.password_recovery_requested_non_existent',

EMAIL_FAILED = 'system.email.failed',
// More events to come....
}
Expand Down
4 changes: 2 additions & 2 deletions api/test/integration/auth/sign-up.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('Create Users', () => {
isActive: true,
});
const { secret, expiresIn } = apiConfig.getJWTConfigByType(
TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION,
TOKEN_TYPE_ENUM.SIGN_UP,
);

const token = jwtService.sign({ id: user.id }, { secret, expiresIn });
Expand All @@ -48,7 +48,7 @@ describe('Create Users', () => {
.request()
.get(authContract.validateToken.path)
.set('Authorization', `Bearer ${token}`)
.query({ tokenType: TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION });
.query({ tokenType: TOKEN_TYPE_ENUM.SIGN_UP });

expect(response.status).toBe(HttpStatus.UNAUTHORIZED);
});
Expand Down
10 changes: 10 additions & 0 deletions shared/contracts/auth.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { JSONAPIError } from "@shared/dtos/json-api.error";
import { TokenTypeSchema } from "@shared/schemas/auth/token-type.schema";
import { z } from "zod";
import { BearerTokenSchema } from "@shared/schemas/auth/bearer-token.schema";
import { SignUpSchema } from "@shared/schemas/auth/sign-up.schema";

// TODO: This is a scaffold. We need to define types for responses, zod schemas for body and query param validation etc.

Expand All @@ -19,6 +20,15 @@ export const authContract = contract.router({
},
body: LogInSchema,
},
signUp: {
method: "POST",
path: "/authentication/sign-up",
responses: {
201: contract.type<null>(),
401: contract.type<JSONAPIError>(),
},
body: SignUpSchema,
},
validateToken: {
method: "GET",
path: "/authentication/validate-token",
Expand Down
17 changes: 17 additions & 0 deletions shared/schemas/auth/sign-up.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from "zod";
import { CreateUserSchema } from "@shared/schemas/users/create-user.schema";

export const SignUpSchema = z.object({
password: z
.string({ message: "Password is required" })
.min(1, "Password is required")
.min(8, "Password must be more than 8 characters")
.max(32, "Password must be less than 32 characters"),
newPassword: z
.string({ message: "Password is required" })
.min(1, "Password is required")
.min(8, "Password must be more than 8 characters")
.max(32, "Password must be less than 32 characters"),
});

export type SignUpDto = z.infer<typeof SignUpSchema>;
2 changes: 1 addition & 1 deletion shared/schemas/auth/token-type.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { z } from "zod";
export enum TOKEN_TYPE_ENUM {
ACCESS = "access",
RESET_PASSWORD = "reset-password",
EMAIL_CONFIRMATION = "email-confirmation",
SIGN_UP = "sign-up",
}

export const TokenTypeSchema = z.object({
Expand Down

0 comments on commit 719eaca

Please sign in to comment.