Skip to content

Commit

Permalink
feat: resendEmail (#145)
Browse files Browse the repository at this point in the history
* feat: resend email route

* fix: resendEmail tests and more

* tournamentId validator
* tournamentId typed better
* added tests for resendEmail route
* updated some tests accordingly

* fix: reverted tournament changes

* removed various errors and failes tests

* refactor: put resend mail directly in the controller

---------

Co-authored-by: Teddy Roncin <[email protected]>
  • Loading branch information
Pulpss and Teddy Roncin authored Oct 13, 2023
1 parent fad7943 commit 9bbbf74
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 4 deletions.
2 changes: 2 additions & 0 deletions src/controllers/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import validate from './validate';
import login from './login';
import askResetPassword from './askResetPassword';
import resetPassword from './resetPassword';
import resendEmail from './resendEmail';

const router = Router();

Expand All @@ -12,5 +13,6 @@ router.post('/validate/:token', validate);
router.post('/login', login);
router.post('/reset-password/ask', askResetPassword);
router.post('/reset-password/:token', resetPassword);
router.post('/resendEmail', resendEmail);

export default router;
25 changes: 25 additions & 0 deletions src/controllers/auth/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,31 @@
400:
$ref: '#/components/responses/400Errored'

/auth/resendEmail:
post:
summary: Renvoie le mail de confirmation d'inscription
description: Renvoie le mail de confirmation d'inscription à l'utilisateur.<br/>
*L'utilisateur ne doit pas être connecté*
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
format: email
username:
type: string
password:
type: string
responses:
204:
description: Le mail a bien été renvoyé
400:
$ref: '#/components/responses/400Errored'

/auth/register:
post:
summary: Crée un nouvel utilisateur
Expand Down
78 changes: 78 additions & 0 deletions src/controllers/auth/resendEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { NextFunction, Request, Response } from 'express';
import Joi from 'joi';
import bcrpyt from 'bcryptjs';
import * as Sentry from '@sentry/node';
import { isNotAuthenticated } from '../../middlewares/authentication';
import { validateBody } from '../../middlewares/validation';
import * as validators from '../../utils/validators';
import { forbidden, success, unauthenticated } from '../../utils/responses';
import { Error as ResponseError } from '../../types';
import { fetchUser } from '../../operations/user';
import { sendValidationCode } from '../../services/email';
import logger from '../../utils/logger';

export default [
// Middlewares
...isNotAuthenticated,
validateBody(
Joi.object({
username: validators.username.required(),
email: validators.email.required(),
password: validators.password.required(),
}),
),

// Controller
async (request: Request, response: Response, next: NextFunction) => {
try {
const { username, password } = request.body;

// Fetch the user depending on the email or the username
if (validators.username.validate(username).error) {
return unauthenticated(response, ResponseError.InvalidCredentials);
}

const field = 'username';

const user = await fetchUser(username, field);

// Checks if the user exists
if (!user) {
return unauthenticated(response, ResponseError.InvalidCredentials);
}

// Checks if the user doesn't already have its email confirmed
if (!user.registerToken) {
return forbidden(response, ResponseError.EmailAlreadyConfirmed);
}

// Compares the hash from the password given
const isPasswordValid = await bcrpyt.compare(password, user.password);

// If the password is not valid, rejects the request
if (!isPasswordValid) {
return unauthenticated(response, ResponseError.InvalidCredentials);
}

// Send registration token by mail
// Don't send sync when it is not needed
// If the mail is not sent, the error will be reported through Sentry
// and staff may resend it manually
sendValidationCode(user).catch((error) => {
Sentry.captureException(error, {
user: {
id: user.id,
username: user.username,
email: user.email,
},
});
logger.warn(error);
});
return success(response, {
sent: true,
});
} catch (error) {
return next(error);
}
},
];
5 changes: 4 additions & 1 deletion src/controllers/teams/getTeams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ export default [
// Controller
async (request: Request, response: Response, next: NextFunction) => {
try {
const { tournamentId, locked } = request.query as { tournamentId: string; locked: string };
const { tournamentId, locked } = request.query as {
tournamentId: string;
locked: string;
};
const lockedCasted = Boolean(locked);

const teams = await fetchTeams(tournamentId);
Expand Down
4 changes: 2 additions & 2 deletions src/services/email/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import nodemailer from 'nodemailer';
import { DetailedCart, EmailAttachement, RawUser } from '../../types';
import { DetailedCart, EmailAttachement, RawUser, User } from '../../types';
import env from '../../utils/env';
import logger from '../../utils/logger';
import { generateTicketsEmail, generateValidationEmail, generatePasswordResetEmail } from './serializer';
Expand Down Expand Up @@ -73,7 +73,7 @@ export const sendPaymentConfirmation = async (cart: DetailedCart) => {
* @throws an error if the mail declared above (corresponding to this
* request) is invalid ie. contains an object which is not a {@link Component}
*/
export const sendValidationCode = async (user: RawUser) => sendEmail(await generateValidationEmail(user));
export const sendValidationCode = async (user: RawUser | User) => sendEmail(await generateValidationEmail(user));

/**
* Sends an email to the user with a password reset link.
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export const enum Error {
NotAdmin = "Tu n'es pas administrateur",
ShopNotAllowed = 'La billetterie est fermée',
EmailNotConfirmed = "Le compte n'est pas confirmé",
EmailAlreadyConfirmed = 'Le compte est déjà confirmé',
NoPermission = "Tu n'as pas la permission d'accéder à cette ressource",
NotCaptain = "Tu dois être le capitaine de l'équipe pour modifier cette ressource",
NotSelf = 'Tu ne peux pas modifier les information de cette personne',
Expand Down
84 changes: 84 additions & 0 deletions tests/auth/resendEmail.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import request from 'supertest';
import app from '../../src/app';
import * as userOperations from '../../src/operations/user';
import * as mailOperations from '../../src/services/email';
import { Error, User } from '../../src/types';
import database from '../../src/services/database';
import { sandbox } from '../setup';
import { createFakeUser } from '../utils';

describe('POST /auth/resendEmail', () => {
const password = 'bonjour123456';
let user: User;
let confirmedUser: User;

before(async () => {
// Creates fake user with email
confirmedUser = await createFakeUser({ password });
user = await createFakeUser({ password, confirmed: false });
});

beforeEach(() => {
sandbox.stub(mailOperations, 'sendEmail');
});

after(async () => {
// Delete the user created
await database.user.deleteMany();
});

it('should return a bad request because of incorrect body', async () => {
await request(app)
.post('/auth/resendEmail')
.send({
fake: 'fake',
})
.expect(400, { error: Error.InvalidUsername });
});

it('should return an error as incorrect credentials (wrong password)', async () => {
await request(app)
.post('/auth/resendEmail')
.send({
username: user.username,
email: user.email,
password: 'wrongpassword',
})
.expect(401, { error: Error.InvalidCredentials });
});

it('should return an internal server error', async () => {
sandbox.stub(userOperations, 'fetchUser').throws('Unexpected error');

await request(app)
.post('/auth/resendEmail')
.send({
username: user.username,
email: user.email,
password,
})
.expect(500, { error: Error.InternalServerError });
});

it('should return an email already confirmed error', async () => {
await request(app)
.post('/auth/resendEmail')
.send({
username: confirmedUser.username,
email: confirmedUser.email,
password,
})
.expect(403, { error: Error.EmailAlreadyConfirmed });
});

it('should return a valid response', async () => {
await request(app)
.post('/auth/resendEmail')
.send({
username: user.username,
email: user.email,
password,
})
.expect(200);
});
});
2 changes: 1 addition & 1 deletion tests/teams/createTeam.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('POST /teams', () => {
.post('/teams')
.send({ ...teamBody, tournamentId: 'factorio' })
.set('Authorization', `Bearer ${token}`)
.expect(404, { error: 'Le tournoi est introuvable' }));
.expect(404, { error: Error.TournamentNotFound }));

it('should fail with an internal server error (test nested check)', () => {
sandbox.stub(teamOperations, 'createTeam').throws('Unexpected error');
Expand Down

0 comments on commit 9bbbf74

Please sign in to comment.