Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reset token and mailer #117

Merged
merged 11 commits into from
May 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"dependencies": {
"@faker-js/faker": "^6.1.2",
"@nestjs-modules/mailer": "^1.6.1",
"@nestjs/common": "^8.4.4",
"@nestjs/config": "^2.0.0",
"@nestjs/core": "^8.4.4",
Expand Down Expand Up @@ -55,6 +56,7 @@
"mongodb": "4.5.0",
"mongoose": "^6.2.10",
"mongoose-lean-virtuals": "^0.9.0",
"nodemailer": "^6.7.5",
"passport": "^0.5.2",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
Expand Down
6 changes: 6 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { VotesModule } from './modules/votes/votes.module';
import { configuration } from './infrastructure/config/configuration';
import AzureModule from './modules/azure/azure.module';
import TeamsModule from './modules/teams/teams.module';
import EmailModule from './modules/mailer/mailer.module';

const imports = [
AppConfigModule,
Expand All @@ -23,6 +24,7 @@ const imports = [
CardsModule,
CommentsModule,
VotesModule,
EmailModule,
TeamsModule,
ScheduleModule.forRoot(),
];
Expand All @@ -31,6 +33,10 @@ if (configuration().azure.enabled) {
imports.push(AzureModule);
}

if (configuration().smtp.enabled) {
imports.push(EmailModule);
}

@Module({
imports,
controllers: [],
Expand Down
21 changes: 18 additions & 3 deletions backend/src/infrastructure/config/config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,33 @@ const NODE_ENV = process.env.NODE_ENV;
AZURE_CLIENT_ID: Joi.any().when('AZURE_ENABLE', {
is: 'true',
then: Joi.required(),
otherwise: Joi.optional(),
}),
AZURE_CLIENT_SECRET: Joi.any().when('AZURE_ENABLE', {
is: 'true',
then: Joi.required(),
otherwise: Joi.optional(),
}),
AZURE_TENANT_ID: Joi.any().when('AZURE_ENABLE', {
is: 'true',
then: Joi.required(),
otherwise: Joi.optional(),
}),
SMTP_ENABLE: Joi.string().required(),
SMTP_HOST: Joi.any().when('SMTP_ENABLE', {
is: 'true',
then: Joi.required(),
}),
SMTP_PORT: Joi.any().when('SMTP_ENABLE', {
is: 'true',
then: Joi.string().required(),
}),
SMTP_USER: Joi.any().when('SMTP_ENABLE', {
is: 'true',
then: Joi.required(),
}),
SMTP_PASSWORD: Joi.any().when('SMTP_ENABLE', {
is: 'true',
then: Joi.required(),
}),
NEXT_PUBLIC_NEXTAUTH_URL: Joi.string().required(),
}),
}),
],
Expand Down
10 changes: 10 additions & 0 deletions backend/src/infrastructure/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export const configuration = (): Configuration => {
port:
parseInt(process.env.BACKEND_PORT as string, 10) || DEFAULT_SERVER_PORT,
},
frontend: {
url: process.env.NEXT_PUBLIC_NEXTAUTH_URL as string,
},
database: {
uri:
NODE_ENV === 'dev'
Expand Down Expand Up @@ -37,6 +40,13 @@ export const configuration = (): Configuration => {
tenantId: process.env.AZURE_TENANT_ID as string,
enabled: process.env.AZURE_ENABLE === 'true',
},
smtp: {
host: process.env.SMTP_HOST as string,
port: process.env.SMTP_PORT as unknown as number,
user: process.env.SMTP_USER as string,
password: process.env.SMTP_PASSWORD as string,
enabled: process.env.STMP_ENABLE === 'true',
},
};

return defaultConfiguration;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { AzureConfiguration } from './azure.configuration.interface';

export interface Configuration extends AzureConfiguration {
smtp: {
enabled: boolean;
host: string;
port: number;
user: string;
password: string;
};
server: {
port: number;
};
Expand Down
7 changes: 7 additions & 0 deletions backend/src/infrastructure/database/mongoose.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import TeamUser, {
TeamUserSchema,
} from '../../modules/teams/schemas/team.user.schema';
import Team, { TeamSchema } from '../../modules/teams/schemas/teams.schema';
import ResetPassword, {
ResetPasswordSchema,
} from '../../modules/auth/schemas/reset-password.schema';
import Schedules, {
SchedulesSchema,
} from '../../modules/schedules/schemas/schedules.schema';
Expand All @@ -24,6 +27,10 @@ export const mongooseUserModule = MongooseModule.forFeature([
{ name: User.name, schema: UserSchema },
]);

export const mongooseResetModule = MongooseModule.forFeature([
{ name: ResetPassword.name, schema: ResetPasswordSchema },
]);

export const mongooseTeamModule = MongooseModule.forFeature([
{ name: Team.name, schema: TeamSchema },
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Inject, Injectable } from '@nestjs/common';
import { CreateResetTokenAuthApplication } from '../interfaces/applications/create-reset-token.auth.application.interface';
import { TYPES } from '../interfaces/types';
import CreateResetTokenAuthService from '../services/create-reset-token.auth.service';

@Injectable()
export class CreateResetTokenAuthApplicationImpl
implements CreateResetTokenAuthApplication
{
constructor(
@Inject(TYPES.services.CreateResetTokenAuthService)
private createResetTokenAuthService: CreateResetTokenAuthService,
) {}

create(emailAddress: string) {
return this.createResetTokenAuthService.create(emailAddress);
}
}
6 changes: 6 additions & 0 deletions backend/src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { mongooseResetModule } from 'src/infrastructure/database/mongoose.module';
import AuthController from './controller/auth.controller';
import UsersModule from '../users/users.module';
import LocalStrategy from './strategy/local.strategy';
import JwtStrategy from './strategy/jwt.strategy';
import JwtRefreshTokenStrategy from './strategy/refresh.strategy';
import {
getTokenAuthService,
createResetTokenAuthService,
createResetTokenAuthApplication,
registerAuthService,
validateUserAuthService,
getTokenAuthApplication,
Expand All @@ -25,13 +28,16 @@ import { JwtRegister } from '../../infrastructure/config/jwt.register';
BoardsModule,
PassportModule,
ConfigModule,
mongooseResetModule,
],
providers: [
getTokenAuthService,
registerAuthService,
validateUserAuthService,
getTokenAuthApplication,
registerAuthApplication,
createResetTokenAuthApplication,
createResetTokenAuthService,
LocalStrategy,
JwtStrategy,
JwtRefreshTokenStrategy,
Expand Down
12 changes: 12 additions & 0 deletions backend/src/modules/auth/auth.providers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { CreateResetTokenAuthApplicationImpl } from './applications/create-reset-token.auth.application';
import { GetTokenAuthApplicationImpl } from './applications/get-token.auth.application';
import { RegisterAuthApplicationImpl } from './applications/register.auth.application';
import { TYPES } from './interfaces/types';
import AzureAuthServiceImpl from '../azure/services/cron.azure.service';
import CreateResetTokenAuthServiceImpl from './services/create-reset-token.auth.service';
import GetTokenAuthServiceImpl from './services/get-token.auth.service';
import RegisterAuthServiceImpl from './services/register.auth.service';
import ValidateUserAuthServiceImpl from './services/validate-user.auth.service';
Expand All @@ -20,6 +22,11 @@ export const validateUserAuthService = {
useClass: ValidateUserAuthServiceImpl,
};

export const createResetTokenAuthService = {
provide: TYPES.services.CreateResetTokenAuthService,
useClass: CreateResetTokenAuthServiceImpl,
};

export const azureAuthService = {
provide: TYPES.services.AzureAuthService,
useClass: AzureAuthServiceImpl,
Expand All @@ -34,3 +41,8 @@ export const registerAuthApplication = {
provide: TYPES.applications.RegisterAuthApplication,
useClass: RegisterAuthApplicationImpl,
};

export const createResetTokenAuthApplication = {
provide: TYPES.applications.CreateResetTokenAuthApplication,
useClass: CreateResetTokenAuthApplicationImpl,
};
8 changes: 8 additions & 0 deletions backend/src/modules/auth/controller/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { GetUserApplication } from '../../users/interfaces/applications/get.user
import JwtAuthenticationGuard from '../../../libs/guards/jwtAuth.guard';
import { GetBoardApplicationInterface } from '../../boards/interfaces/applications/get.board.application.interface';
import { GetTeamApplicationInterface } from '../../teams/interfaces/applications/get.team.application.interface';
import { CreateResetTokenAuthApplication } from '../interfaces/applications/create-reset-token.auth.application.interface';

@Controller('auth')
export default class AuthController {
Expand All @@ -46,6 +47,8 @@ export default class AuthController {
private getTeamsApp: GetTeamApplicationInterface,
@Inject(Boards.TYPES.applications.GetBoardApplication)
private getBoardApp: GetBoardApplicationInterface,
@Inject(TYPES.applications.CreateResetTokenAuthApplication)
private createResetTokenAuthApp: CreateResetTokenAuthApplication,
) {}

@Post('register')
Expand Down Expand Up @@ -88,6 +91,11 @@ export default class AuthController {
return this.getUserApp.getByEmail(email).then((user) => !!user);
}

@Post('recoverPassword')
forgot(@Body() { email }: EmailParam) {
return this.createResetTokenAuthApp.create(email);
}

@UseGuards(JwtAuthenticationGuard)
@Get('/dashboardStatistics')
async getDashboardHeaderInfo(@Req() request: RequestWithUser) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface CreateResetTokenAuthApplication {
create(emailAddress: string): Promise<{
message: string;
}>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface CreateResetTokenAuthService {
create(emailAddress: string): Promise<{
message: string;
}>;
}
2 changes: 2 additions & 0 deletions backend/src/modules/auth/interfaces/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ export const TYPES = {
GetTokenAuthService: 'GetTokenAuthService',
ValidateAuthService: 'ValidateAuthService',
RegisterAuthService: 'RegisterAuthService',
CreateResetTokenAuthService: 'CreateResetTokenAuthService',
AzureAuthService: 'AzureAuthService',
},
applications: {
GetTokenAuthApplication: 'GetTokenAuthApplication',
RegisterAuthApplication: 'RegisterAuthApplication',
CreateResetTokenAuthApplication: 'CreateResetTokenAuthApplication',
},
};
18 changes: 18 additions & 0 deletions backend/src/modules/auth/schemas/reset-password.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as mongoose from 'mongoose';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';

export type ResetPasswordDocument = ResetPassword & mongoose.Document;

@Schema()
export default class ResetPassword {
@Prop({ nullable: false, unique: true })
emailAddress!: string;

@Prop({ nullable: true, unique: true })
token?: string;

@Prop({ default: Date.now })
updatedAt!: Date;
}

export const ResetPasswordSchema = SchemaFactory.createForClass(ResetPassword);
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { MailerService } from '@nestjs-modules/mailer';
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectModel } from '@nestjs/mongoose';
import { Model, ClientSession } from 'mongoose';
import { CreateResetTokenAuthService } from '../interfaces/services/create-reset-token.auth.service.interface';
import ResetPassword, {
ResetPasswordDocument,
} from '../schemas/reset-password.schema';

@Injectable()
export default class CreateResetTokenAuthServiceImpl
implements CreateResetTokenAuthService
{
constructor(
@InjectModel(ResetPassword.name)
private resetModel: Model<ResetPasswordDocument>,
private mailerService: MailerService,
private configService: ConfigService,
) {}

public async emailBody(token: string, emailAddress: string) {
const url = `${this.configService.get<string>('frontend.url')}?${token}`;
const msg = 'please check your email';
await this.mailerService.sendMail({
to: emailAddress,
subject: 'You requested a password reset',
html: `Trouble signing in?
Resetting your password is easy.

Just click <a href ="${url}"> here </a> to reset your password. We’ll have you up and running in no time.

If you did not make this request then please ignore this email.`,
});
return { message: msg };
}

public tokenGenerator(emailAddress: string, session: ClientSession) {
const genToken = (Math.floor(Math.random() * 9000000) + 1000000).toString();
return this.resetModel
.findOneAndUpdate(
{ emailAddress },
{
emailAddress,
token: genToken,
updatedAt: new Date(),
},
{ upsert: true, new: true },
)
.session(session)
.lean()
.exec();
}

public tokenValidator(updatedAt: Date) {
const isTokenInvalid =
(new Date().getTime() - updatedAt.getTime()) / 60000 < 1;
if (isTokenInvalid) {
throw new Error('EMAIL_SENDED_RECENTLY');
}
}

async create(emailAddress: string) {
const session = await this.resetModel.db.startSession();
session.startTransaction();
try {
const passwordModel = await this.resetModel.findOne({ emailAddress });
if (passwordModel) {
this.tokenValidator(passwordModel.updatedAt);
}
const { token } = await this.tokenGenerator(emailAddress, session);
if (token) {
const res = await this.emailBody(token, emailAddress);
await session.commitTransaction();
return res;
}
throw new InternalServerErrorException();
} catch (e) {
await session.abortTransaction();
return { message: e.message };
} finally {
await session.endSession();
}
}
}
Loading