I’ve recently got myself in a problem while I was doing a personal project using Angular, NodeJS and MySQL.
I was doing the sign up and log in processes and I decided to encrypt password using bycrptjs
.
The problem is that it is not working because when the sign up form is submitted it shows me an encrypted password but after I verified my account and went to the log in the problem came when I received my personal alert:
Password doesn't match! 4 attempts remaining!
I didn’t understood why it came such response, because I inserted “Password@1234” for both sign up and log in as shown in the images below:
Above is the sign up page
Above is the log in page
I try to verify this by logging into the server console both passwords and it came like this:
Password stored in DB: $2a$12$7c4L8iqE2d3EpFenFoaxI.iWEvWybPy1Ab8Dxe7Qv4pNOcByNEC1i
Inputted password: $2a$12$Z04YqWKs7YVJ8aYDumuDBuPSpE/H90SAUR0hiosmUw6iJ4Hh7NUCa
And then, when I tried to surf in Internet I realized that I cannot convert those encrypted passwords due to security purposes.
Here’s my Angular code:
Front End
sign-up.component.ts:
import { CommonModule, DatePipe } from '@angular/common';
import { HttpClient, HttpErrorResponse, HttpClientModule } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
import * as bcrypt from 'bcryptjs';
interface SignUpResponse {
message: string;
status: number
}
@Component({
selector: 'app-sign-up',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, HttpClientModule],
templateUrl: './sign-up.component.html',
styleUrl: './sign-up.component.css'
})
export class SignUpComponent implements OnInit {
hide: boolean = true;
signUpForm!: FormGroup;
logInForm!: FormGroup;
signUpSuccess: boolean = false;
verifyPinForm: any;
alertPlaceholder1!: HTMLDivElement;
alertPlaceholder2!: HTMLDivElement;
constructor(private httpClient: HttpClient, private fb: FormBuilder) {}
ngOnInit(): void {
this.signUpForm = this.fb.group({
userName: ['', Validators.required],
userEmail: ['', [Validators.required, Validators.email]],
userContact: ['', Validators.required],
userDOB: ['', [Validators.required, minimumAgeValidator(18)]],
userPwd: ['', [Validators.required, Validators.minLength(8), this.passwordStrengthValidator()]],
confirmPwd: ['', [Validators.required, Validators.minLength(8), this.passwordStrengthValidator()]]
}, { validators: this.fieldsMatchValidator('userPwd', 'confirmPwd') });
this.verifyPinForm = this.fb.group({
pin: ['', Validators.required]
});
this.alertPlaceholder1 = document.getElementById('liveAlertPlaceholder1') as HTMLDivElement;
this.alertPlaceholder2 = document.getElementById('liveAlertPlaceholder2') as HTMLDivElement;
}
passwordStrengthValidator(): ValidatorFn {
return Validators.pattern('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[\W_])[A-Za-z\d\W_].{8,}$');
}
fieldsMatchValidator(...fields: string[]): ValidatorFn {
return (group: AbstractControl): { [key: string]: any } | null => {
let isValid = true;
for (let i = 0; i < fields.length; i += 2) {
let field = group.get(fields[i]);
let matchingField = group.get(fields[i + 1]);
if (field && matchingField && field.value !== matchingField.value) {
isValid = false;
matchingField.setErrors({ fieldsDoNotMatch: true });
} else {
matchingField?.setErrors(null);
}
}
return isValid ? null : { 'fieldsDoNotMatch': true };
};
}
encryptPassword(password: string): string {
const saltRounds = 12;
const salt = bcrypt.genSaltSync(saltRounds);
const hash = bcrypt.hashSync(password, salt);
return hash;
}
signUp(): void {
if (this.signUpForm.valid) {
const formWithEncryptedPwd = {
...this.signUpForm.value,
confirmPwd: null,
userPwd: this.encryptPassword(this.signUpForm.value.userPwd)
};
this.httpClient.post('http://localhost:3000/sign-up', formWithEncryptedPwd).subscribe(
(response) => {
const message = (response as SignUpResponse).message;
this.appendAlert(message, "success", 1);
this.signUpSuccess = true;
},
(error: HttpErrorResponse) => {
this.appendAlert(error.error.message, "danger", 1)
}
);
}
}
verifyPin(): void {
const pin = this.verifyPinForm.get('pin')?.value;
if (pin) {
this.httpClient
.post('http://localhost:3000/verify-pin', {
userName: this.signUpForm.value.userName,
verificationPin: pin,
})
.subscribe(
(response: any) => {
this.appendAlert(response.message, 'success', 2);
},
(error: HttpErrorResponse) => {
this.appendAlert(error.error.message, 'danger', 2);
}
);
}
}
appendAlert = (message: any, type: any, option: number): void => {
const wrapper = document.createElement('div')
if (type === 'success') {
wrapper.innerHTML = [
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
` <div><i class="bi bi-check-circle-fill"></i> ${message}</div>`,
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
'</div>'
].join('')
} else {
wrapper.innerHTML = [
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
` <div><i class="bi bi-x-circle-fill"></i> ${message}</div>`,
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
'</div>'
].join('')
}
switch (option) {
case 1:
this.alertPlaceholder1.append(wrapper);
break;
case 2:
this.alertPlaceholder2.append(wrapper);
break;
default:
alert("ERROR! SOMETHING WENT WRONG!")
break;
}
}
}
export function minimumAgeValidator(minAge: number) {
return (control: AbstractControl): ValidationErrors | null => {
const birthDate = new Date(control.value);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDifference = today.getMonth() - birthDate.getMonth();
if (monthDifference < 0 || (monthDifference === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age >= minAge ? null : { 'minimumAge': { value: minAge } };
};
}
log-in.component.ts:
import { CommonModule } from '@angular/common';
import { HttpClient, HttpErrorResponse, HttpClientModule } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, ValidatorFn, Validators } from '@angular/forms';
import * as bcrypt from 'bcryptjs';
interface LogInResponse {
message: string;
status: number
}
@Component({
selector: 'app-log-in',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, HttpClientModule],
templateUrl: './log-in.component.html',
styleUrl: './log-in.component.css'
})
export class LogInComponent implements OnInit{
logInForm!: FormGroup;
hide: boolean = true;
logInSuccess: boolean = false;
alertPlaceholder!: HTMLDivElement;
constructor(private httpClient: HttpClient, private fb: FormBuilder) { }
ngOnInit(): void {
this.logInForm = this.fb.group({
userName: ['', Validators.required],
userPwd: ['', [Validators.required, Validators.minLength(8), this.passwordStrengthValidator()]],
});
this.alertPlaceholder = document.getElementById('liveAlertPlaceholder3') as HTMLDivElement;
}
passwordStrengthValidator(): ValidatorFn {
return Validators.pattern('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[\W_])[A-Za-z\d\W_].{8,}$');
}
encryptPassword(password: string): string {
const saltRounds = 12;
const salt = bcrypt.genSaltSync(saltRounds);
const hash = bcrypt.hashSync(password, salt);
return hash;
}
logIn(): void {
if (this.logInForm.valid) {
const encryptedPassword = this.encryptPassword(this.logInForm.value.userPwd);
const formWithEncryptedPassword = {
...this.logInForm.value,
userPwd: encryptedPassword
};
this.httpClient.post('http://localhost:3000/log-in', formWithEncryptedPassword).subscribe(
(response) => {
const message = (response as LogInResponse).message;
this.appendAlert(message, "success", 1);
this.logInSuccess = true;
},
(error: HttpErrorResponse) => {
this.appendAlert(error.error.message, "danger", 1)
if (error.error.message.includes('Account locked')) {
this.logInForm.disable();
setTimeout(() => this.logInForm.enable(), 300000); // 5 minutes
}
}
);
}
}
appendAlert = (message: any, type: any, option: number): void => {
const wrapper = document.createElement('div')
if (type === 'success') {
wrapper.innerHTML = [
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
` <div><i class="bi bi-check-circle-fill"></i> ${message}</div>`,
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
'</div>'
].join('')
} else {
wrapper.innerHTML = [
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
` <div><i class="bi bi-x-circle-fill"></i> ${message}</div>`,
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
'</div>'
].join('')
}
switch (option) {
case 1:
this.alertPlaceholder.append(wrapper);
break;
default:
alert("ERROR! SOMETHING WENT WRONG!")
break;
}
}
}
Back End
sign-up.js:
"use strict"
const express = require('express');
const router = express.Router();
const connection = require('./db/connection');
const crypto = require('crypto');
const nodemailer = require('nodemailer');
// Environment variables should be used here
const emailUser = process.env.EMAIL_USER;
const emailPass = process.env.EMAIL_PASS;
// Configure the email transport using the default SMTP transport and your email account details
const transporter = nodemailer.createTransport({
service: 'gmail', // Use your email provider
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: {
user: emailUser, // Your email address
pass: emailPass // Your email password
},
tls: {
rejectUnauthorized: true,
}
});
// Sign-up endpoint
router.post('/sign-up', (req, res) => {
const { userName, userEmail, userContact, userDOB, userPwd} = req.body;
if (!userName || !userEmail || !userContact || !userDOB || !userPwd ) {
return res.status(400).json({ error: 'All fields are required' });
}
const verificationPin = crypto.randomInt(100000, 1000000).toString();
const isVerified = 0;
const sql = 'INSERT INTO users (name, email, contactNumber, dob, pwd, verificationPin, isVerified) VALUES (?, ?, ?, ?, ?, ?, ?)';
const userValues = [userName, userEmail, userContact, userDOB, userPwd, verificationPin, isVerified];
connection.execute(sql, userValues, (err, _results) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Error while inserting data' });
} else {
sendEmail(userEmail, userName, verificationPin, (emailError) => {
if (emailError) {
return res.status(500).json({ message: 'Something went wrong while sending email' });
} else {
return res.status(200).json({ message: 'User has been created successfully! Now check your email to insert PIN below' });
}
});
}
});
});
let sendEmail = (usrEmail, usrName, verifyPin, callback) => {
const mailOptions = {
from: emailUser,
to: usrEmail,
subject: 'Verify Your Account',
text: `Hello ${usrName},nnWelcome to BeQuick!nnYour verification PIN is ${verifyPin}. Please enter this PIN to verify your account.nnThank you!nBeQuick Company.`
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
console.error('Error while sending email:', error);
callback(error, null);
} else {
console.log('Email sent:', info.response);
callback(null, info.response);
}
});
};
module.exports = router;
log-in.js:
"use strict";
const express = require('express');
const router = express.Router();
const connection = require('./db/connection');
const crypto = require('crypto');
const nodemailer = require('nodemailer');
const bcrypt = require('bcryptjs');
// Environment variables should be used here
const emailUser = process.env.EMAIL_USER;
const emailPass = process.env.EMAIL_PASS;
// Log-in endpoint
router.post('/log-in', (req, res) => {
const { userName, userPwd } = req.body;
const sql = 'SELECT * FROM users WHERE name = ?';
connection.execute(sql, [userName], (err, results) => {
if (err) {
console.error('Error while logging: ', err);
return res.status(500).json({ error: 'Oops! Something went wrong! Please try again later.' });
} else {
if (results.length === 0) {
return res.status(400).json({ message: `Username doesn't exist! You may sign up or try again.` });
} else {
const user = results[0];
const now = new Date();
if (user.lock_until && now < new Date(user.lock_until)) {
return res.status(400).json({ message: `Account locked. Try again later. Account will be unlocked at ${user.lock_until}`});
} else {
console.log("Password stored in DB: "+user.pwd);
console.log("Inputted password: "+userPwd);
if (bcrypt.compareSync(userPwd, user.pwd)) {
connection.execute('UPDATE users SET failed_attempts = 0, lock_until = NULL WHERE name = ?', [userName]);
return res.status(200).json({ message: 'You successfully logged in!' });
}
else {
let failedAttempts = user.failed_attempts + 1;
let lockUntil = null;
if (failedAttempts >= 5) {
lockUntil = new Date(now.getTime() + 5 * 60000); // 5 minutes lock
}
connection.execute('UPDATE users SET failed_attempts = ?, lock_until = ? WHERE name = ?', [failedAttempts, lockUntil, userName]);
return res.status(400).json({ message: `Password doesn't match! ${5 - failedAttempts} attempts remaining!` });
}
}
}
}
});
});
module.exports = router;
connection.js:
"use strict"
require('dotenv').config();
const mysql = require('mysql2');
//MySQL connection
const connection = mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME
});
connection.connect(err => {
if (err) throw err;
console.log('Connected to the MySQL server.');
});
module.exports = connection;
I am not understanding why does it behave like this.
If you need anything else, please let me know in comments.
Any help would be appreciated. Thanks!
2