Skip to content

Commit

Permalink
feat(backend/frontend/shared): forgot-password-functionality bb-188 (#…
Browse files Browse the repository at this point in the history
…230)

* feat: first implementation of email-sending part bb-188

* feat: changed forgot password endpooint return value bb-188

* fix: fixing default_forgot_password_payload import bb-188

* fix: renamed back to constants.ts bb-188

* refactor: moved default_payload consts for auth forms into constants folders bb-188

* feat: forgot-password endpoints don't return a value bb-188

* feat: added swagger doc for forgot-password reset-password enpoints bb-188

* refactor: added patch method to repository bb-188

* refactor: addressed own comments on pull request bb-188

* refactor: merged main bb-188

* refactor: addressed pr comment changes, removed patch payload bb-188

* refactor: changed naming for service and repository from findbyid to find bb-188

* refactor: addressed pr comments bb-188

* fix: removed .js import from sign up form on mobile bb-188

* fix: brought react import back to sign up form on mobile bb-188

* refactor: created a separate type for reset-password-form validation inside of use app form bb-188

* refactor: intermediate commit bb-188

* refactor: addressed pr comments bb-188

* refactor: self-assesment fixes bb-188

* fix: brought back exports for UserValidationMessage bb-188

* refactor: addressed pull request comments bb-188

* feat: added toggle visibility for reset password inputs bb-188

* refactor: addressed pull request comments bb-188

* refactor: merged main bb-188

* fix: removed react import from use-query hook definition bb-188

* refactor: addressed pull request comments bb-188

* refactor: rewritten mailer to spread config.ENV.MAILER when creating mailer instance bb-188

* refactor: renamed inputs to settings bb-188

* refactor: created a new type for authRouteToHeader key bb-188

* refactor: merged main bb-188

* refactor: adressed pull request comments bb-188

* refactor: adressed pull request comments bb-188

* fix: fixed typo bb-188

---------

Co-authored-by: Farid Shabanov <[email protected]>
  • Loading branch information
ihorLysak and fshabanov authored Sep 3, 2024
1 parent 7803eca commit 3e24ec9
Show file tree
Hide file tree
Showing 64 changed files with 1,877 additions and 932 deletions.
14 changes: 14 additions & 0 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ NODE_ENV=development
PORT=3001
HOST=localhost

#
# BASE_URLS
#
RESET_PASSWORD_URL=http://[host]:[port]/reset-password

#
# DATABASE
#
Expand All @@ -19,3 +24,12 @@ DB_POOL_MAX=10
JWT_SECRET=<SECRET_PHRASE>
JWT_EXPIRATION_TIME=24hr
JWT_ALGORITHM=HS256

#
# MAILER
#
MAILER_ADDRESS=<EMAIL_ADDRESS>
MAILER_APP_PASSWORD=<GOOGLE_APP_PASSWORD>
MAILER_HOST=smtp.gmail.com
MAILER_PORT=465
MAILER_SERVICE=Gmail
2 changes: 2 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"fastify-plugin": "4.5.1",
"jose": "5.6.3",
"knex": "3.1.0",
"nodemailer": "6.9.14",
"objection": "3.1.4",
"pg": "8.12.0",
"pino": "9.3.2",
Expand All @@ -42,6 +43,7 @@
"devDependencies": {
"@types/bcrypt": "5.0.2",
"@types/convict": "6.1.6",
"@types/nodemailer": "6.4.15",
"@types/swagger-jsdoc": "6.0.4",
"ts-node": "10.9.2",
"ts-paths-esm-loader": "1.4.3",
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/libs/exceptions/exceptions.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { MailError } from "./mail-error.exception.js";
export { ValidationError } from "shared";
17 changes: 17 additions & 0 deletions apps/backend/src/libs/exceptions/mail-error.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type HTTPCode } from "~/libs/modules/http/http.js";
import { HTTPError } from "~/libs/modules/http/http.js";
import { type ValueOf } from "~/libs/types/types.js";

type Constructor = {
cause?: unknown;
message: string;
status: ValueOf<typeof HTTPCode>;
};

class MailError extends HTTPError {
public constructor({ cause, message, status }: Constructor) {
super({ cause, message, status });
}
}

export { MailError };
40 changes: 40 additions & 0 deletions apps/backend/src/libs/modules/config/base-config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ class BaseConfig implements Config {
format: Number,
},
},
BASE_URLS: {
RESET_PASSWORD_URL: {
default: null,
doc: "base url for a reset user password link",
env: "RESET_PASSWORD_URL",
format: String,
},
},
DB: {
CONNECTION_STRING: {
default: null,
Expand Down Expand Up @@ -96,6 +104,38 @@ class BaseConfig implements Config {
format: String,
},
},
MAILER: {
ADDRESS: {
default: null,
doc: "Email used to connect to Google's SMTP service",
env: "MAILER_ADDRESS",
format: String,
},
APP_PASSWORD: {
default: null,
doc: "App password generated by Google, used to connect to Google's SMTP service",
env: "MAILER_APP_PASSWORD",
format: String,
},
HOST: {
default: null,
doc: "SMTP service host",
env: "MAILER_HOST",
format: String,
},
PORT: {
default: null,
doc: "SMTP service port",
env: "MAILER_PORT",
format: Number,
},
SERVICE: {
default: null,
doc: "SMTP service provider",
env: "MAILER_SERVICE",
format: String,
},
},
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ type EnvironmentSchema = {
HOST: string;
PORT: number;
};
BASE_URLS: {
RESET_PASSWORD_URL: string;
};
DB: {
CONNECTION_STRING: string;
DIALECT: string;
Expand All @@ -18,6 +21,13 @@ type EnvironmentSchema = {
EXPIRATION_TIME: string;
SECRET: string;
};
MAILER: {
ADDRESS: string;
APP_PASSWORD: string;
HOST: string;
PORT: number;
SERVICE: string;
};
};

export { type EnvironmentSchema };
60 changes: 60 additions & 0 deletions apps/backend/src/libs/modules/mailer/base-mailer.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createTransport, type Transporter } from "nodemailer";

import { ErrorMessage } from "~/libs/enums/enums.js";
import { MailError } from "~/libs/exceptions/exceptions.js";
import { config } from "~/libs/modules/config/config.js";
import { HTTPCode } from "~/libs/modules/http/http.js";

type Constructor = {
address: string;
appPassword: string;
host: string;
port: number;
service: string;
};

class BaseMailer {
private sender = config.ENV.MAILER.ADDRESS;
private transporter: Transporter;

constructor(settings: Constructor) {
this.transporter = createTransport({
auth: {
pass: settings.appPassword,
user: settings.address,
},
host: settings.host,
port: settings.port,
secure: true,
service: settings.service,
});
}

public sendEmail({
subject,
text,
to,
}: {
subject: string;
text: string;
to: string;
}): void {
this.transporter.sendMail(
{
from: this.sender,
subject,
text,
to,
},
(error) => {
throw new MailError({
cause: (error as Error).cause,
message: ErrorMessage.MAIL_ERROR,
status: HTTPCode.INTERNAL_SERVER_ERROR,
});
},
);
}
}

export { BaseMailer };
15 changes: 15 additions & 0 deletions apps/backend/src/libs/modules/mailer/mailer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { config } from "~/libs/modules/config/config.js";

import { BaseMailer } from "./base-mailer.module.js";

const { ADDRESS, APP_PASSWORD, HOST, PORT, SERVICE } = config.ENV.MAILER;

const mailer = new BaseMailer({
address: ADDRESS,
appPassword: APP_PASSWORD,
host: HOST,
port: PORT,
service: SERVICE,
});

export { mailer };
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const WHITE_ROUTES: string[] = [
APIPath.AUTH,
`${APIPath.AUTH}${AuthApiPath.SIGN_IN}`,
`${APIPath.AUTH}${AuthApiPath.SIGN_UP}`,
`${APIPath.AUTH}${AuthApiPath.FORGOT_PASSWORD}`,
`${APIPath.AUTH}${AuthApiPath.RESET_PASSWORD}`,
];

export { WHITE_ROUTES };
78 changes: 78 additions & 0 deletions apps/backend/src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
import { HTTPCode } from "~/libs/modules/http/http.js";
import { type Logger } from "~/libs/modules/logger/logger.js";
import {
type EmailDto,
type ResetPasswordDto,
type UserDto,
userForgotPasswordVaidationSchema,
userResetPasswordValidationSchema,
type UserSignInRequestDto,
userSignInValidationSchema,
type UserSignUpRequestDto,
Expand Down Expand Up @@ -63,6 +67,57 @@ class AuthController extends BaseController {
body: userSignInValidationSchema,
},
});

this.addRoute({
handler: (options) =>
this.forgotPassword(
options as APIHandlerOptions<{
body: EmailDto;
}>,
),
method: "POST",
path: AuthApiPath.FORGOT_PASSWORD,
validation: {
body: userForgotPasswordVaidationSchema,
},
});

this.addRoute({
handler: (options) =>
this.resetPassword(
options as APIHandlerOptions<{
body: ResetPasswordDto;
}>,
),
method: "PATCH",
path: AuthApiPath.RESET_PASSWORD,
validation: {
body: userResetPasswordValidationSchema,
},
});
}

/**
* @swagger
* /auth/forgot-password:
* post:
* description: Return authenticated user
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Successfull operation
*/

private async forgotPassword(
options: APIHandlerOptions<{
body: EmailDto;
}>,
): Promise<APIHandlerResponse> {
return {
payload: await this.authService.forgotPassword(options.body),
status: HTTPCode.OK,
};
}

/**
Expand Down Expand Up @@ -92,6 +147,29 @@ class AuthController extends BaseController {
};
}

/**
* @swagger
* /auth/reset-password:
* post:
* description: Return authenticated user
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Successfull operation
*/

private async resetPassword(
options: APIHandlerOptions<{
body: ResetPasswordDto;
}>,
): Promise<APIHandlerResponse> {
return {
payload: await this.authService.resetPassword(options.body),
status: HTTPCode.OK,
};
}

/**
* @swagger
* /auth/sign-in:
Expand Down
57 changes: 57 additions & 0 deletions apps/backend/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { ErrorMessage } from "~/libs/enums/enums.js";
import { config } from "~/libs/modules/config/config.js";
import { type Encrypt } from "~/libs/modules/encrypt/encrypt.js";
import { mailer } from "~/libs/modules/mailer/mailer.js";
import { token } from "~/libs/modules/token/token.js";
import {
type EmailDto,
type ResetPasswordDto,
type UserSignInRequestDto,
type UserSignInResponseDto,
type UserSignUpRequestDto,
Expand All @@ -21,6 +25,59 @@ class AuthService {
this.encrypt = encrypt;
}

public async forgotPassword(payload: EmailDto): Promise<boolean> {
const { email: targetEmail } = payload;

const user = await this.userService.findByEmail(targetEmail);

if (!user) {
throw new AuthError({
message: ErrorMessage.INCORRECT_CREDENTIALS,
status: HTTPCode.UNAUTHORIZED,
});
}

const userDetails = user.toObject();

const jwtToken = await token.createToken({
userId: userDetails.id,
});

mailer.sendEmail({
subject: "BeBalance: reset password",
text: `Here is a link to reset your password: ${config.ENV.BASE_URLS.RESET_PASSWORD_URL}?token=${jwtToken}`,
to: userDetails.email,
});

return true;
}

public async resetPassword(
payload: ResetPasswordDto,
): Promise<UserSignInResponseDto> {
const { jwtToken, newPassword } = payload;

const {
payload: { userId },
} = await token.decode(jwtToken);

const user = await this.userService.find(userId);

if (!user) {
throw new AuthError({
message: ErrorMessage.INCORRECT_CREDENTIALS,
status: HTTPCode.UNAUTHORIZED,
});
}

await this.userService.changePassword(userId, newPassword);

return {
token: jwtToken,
user: user.toObject(),
};
}

public async signIn(
userRequestDto: UserSignInRequestDto,
): Promise<UserSignInResponseDto> {
Expand Down
Loading

0 comments on commit 3e24ec9

Please sign in to comment.