Models
Before you jump in and start coding the API endpoints, it's worth taking a few minutes to think about what data we need to store and the relationships between the different objects.
Our User Model will have the usual fields
- Username, password, email,
- Profile object containing { 1st, 2nd names, avatar url, bio, phone,...}
- passwordresetToken fields
- jwt Token
Defining and creating models
Models are defined using the Schema interface. The Schema allows you to define the fields stored in each document along with their validation requirements and default values.
backend/models/User.ts
import mongoose from "mongoose";
import bycrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import crypto from 'crypto';
import { model, Schema, Model, Document } from 'mongoose';
//declare point type
export interface IPoint extends Document {
type:string;
coordinates:string;
}
//generate point schema
const Point:Schema= new Schema({
type: {
type: String,
enum: ['Point'],
required: true
},
coordinates: {
type: [Number],
required: true
}
});
//declare user type
export interface IUser extends Document {
getResetPasswordToken():string;
getSignedToken():string;
resetPasswordToken: string|undefined;
resetPasswordExpire: string|undefined;
matchPassword(password: string): boolean | PromiseLike<boolean>;
username:string;
password:string;
email:string;
profile: {
firstName: String,
lastName: String,
avatar: String,
bio: String,
phone: String,
gender: String,
address: {
street1: String,
street2: String,
city: String,
state: String,
country: String,
zip: String,
location: {
type: IPoint,
required: false
}
},
active:true
}
}
// define user schema
const UserSchema: Schema = new Schema({
username: {
type: String,
lowercase: true,
unique: true,
required: [true, "Can't be blank"],
index: true
},
password: {
type: String,
required: true,
select: false,
minlength: [8, "Please use minimum of 8 characters"],
},
email: {
type: String,
lowercase: true,
required: [true, "Can't be blank"],
match: [/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/, 'Please use a valid address'],
unique:true,
index:true
},
profile: {
firstName: String,
lastName: String,
avatar: String,
bio: String,
phone: String,
gender: String,
address: {
street1: String,
street2: String,
city: String,
state: String,
country: String,
zip: String,
location: {
type: Point,
required: false
}
},
required:false
},
resetPasswordToken: String,
resetPasswordExpire: String,
active: { type: Boolean, default: true }
});
UserSchema.pre<IUser>("save", async function (next: any) {
if (!this.isModified('password')) {
return next();
}
const salt = await bycrypt.genSalt(10);
this.password = bycrypt.hashSync(this.password, 10);
next();
});
UserSchema.methods.matchPassword= async function (password:string) {
return await bycrypt.compare(password,this.password)
}
UserSchema.methods.getSignedToken= function (password:string) {
return jwt.sign({id:this._id},process.env.JWT_SECRET!,{
expiresIn:process.env.JWT_EXPIRE
})
}
UserSchema.methods.getResetPasswordToken= function () {
const resetToken= crypto.randomBytes(20).toString('hex');
this.resetPasswordToken= crypto.
createHash('sha256')
.update(resetToken)
.digest('hex');
this.resetPasswordExpire = Date.now() + 10*(60*1000)
return resetToken
}
export const User:Model<IUser> = model("User", UserSchema);
Schema methods help in preforming functions on the data fields of a document. Packages such as 'bycrypt' are used to hash the passwords before storing them in the data base. NEVER STORE RAW PASSWORDS IN THE DATABASE INCASE OF DATA BREACHES WHICH HAPPENS MORE OFTEN THAN YOU THINK
Routes
Routing refers to how an application’s endpoints (URIs) respond to client requests. These routing methods specify a callback function (sometimes called “handler functions”) called when the application receives a request to the specified route (endpoint) and HTTP method. In other words, the application “listens” for requests that match the specified route(s) and method(s), and when it detects a match, it calls the specified callback function.
backend/routes/auth.ts
import express from 'express';
const router= express.Router();
//import controllers
const {register,login,forgotPassword,resetPassword}=require('../controllers/auth');
//routes
router.route('/register').post(register);
router.route('/login').post(login);
router.route('/forgotpassword').post(forgotPassword);
router.route('/resetpassword/:resetToken').post(resetPassword);
module.exports =router;
The routes are used in backend/server.ts file.
app.use("/api/auth", require("./routes/auth"));
The full path is appended onto "api/auth" availing 4 paths:
> api/auth/register
> api/auth/login
> api/auth/forgotpassword
> api/auth/resetpassword (takes a reset token as the parameter)
Controllers
Controller functions to get the requested data from the models, create an HTTP response and return it to the user.
backend/controllers/auth.ts
We create 4 controllers used in backend/routes/auth.ts
register
Create a new User using the model.create() function and pass the required parameters from request body
import { Response, Request } from 'express';
import {IUser, User} from '../models/User';
exports.register= async(req:Request,res:Response,next:any)=>{
const {username,email,password}=req.body;
try {
const user:IUser= await User.create({
username
,email,
password
});
sendToken(user,201,res)
} catch (error:any) {
next(error);
}
};
login
import { Response, Request } from 'express';
import {ErrorResponse} from '../utils/errorResponse';
import {IUser, User} from '../models/User';
exports.login = async(req:Request,res:Response,next:any)=>{
const {email,password}=req.body;
if (!email || !password){
return next(new ErrorResponse("Please provide a valid email and Password",400))
};
try {
const user:IUser | null = await User.findOne({email}).select("+password");
if (!user){
return next(new ErrorResponse("Invalid Credentials",401))
}
const isMatch:boolean= await user.matchPassword(password);
if (!isMatch){
return next(new ErrorResponse("Invalid Credentials",401))
}
sendToken(user,200,res)
} catch (error:any) {
return next(new ErrorResponse(error.message,500))
}
forgotPassword and resetPassword
Here the user methods in our User model are used to generate, verify, and change the resetTokens
exports.forgotPassword=async(req:Request,res:Response,next:any)=>{
const {email}=req.body;
try {
const user:IUser | null= await User.findOne({user:email});
if (!user){
return next(new ErrorResponse("Email could not be sent",404));
}
const resetToken=user.getResetPasswordToken();
await user.save();
const resetUrl = `http://localhost:3000/passwordreset/${resetToken}`;
const message = `
<h1> You have requested a password reset </h1>
<p> Please go to this link to reset your password </p>
<a href=${resetUrl} clicktracking=off>${resetUrl}</a>
`
try {
await sendEmail({
to: user.email,
text:message,
subject:message
});
res.status(200)
.json({
success: true,
data:"Email Sent"
})
} catch (error) {
user.resetPasswordToken=undefined;
user.resetPasswordExpire=undefined;
await user.save();
return next(new ErrorResponse("Email could not be sent", 500))
}
} catch (error) {
next(error);
}
};
exports.resetPassword=async(req:Request,res:Response,next:any)=>{
const {password} = req.body
const resetPasswordToken = crypto.createHash("sha256")
.update(req.params.resetToken)
.digest('hex');
try {
const user: IUser | null = await User.findOne({
resetPasswordToken,
resetPasswordExpire: {$gt: Date.now(),
}
})
if (!user){
return next(new ErrorResponse("Invalid Reset Token", 400));
}
user.password = password;
user.resetPasswordToken=undefined;
user.resetPasswordExpire= undefined;
await user.save();
res.status(201)
.json({
success: true,
data:"Password Reset successful"
});
} catch (error) {
next(error);
}
};
backend/utils
Contains the helper functions used in our controllers to avoid repetition
- errorResponse.ts
export class ErrorResponse extends Error{
statusCode: number;
constructor(message:any,statusCode:number){
super(message);
this.statusCode= statusCode;
}
}
- emailSender.ts
Sends emails upon registration and password reset requests
import nodemailer from 'nodemailer';
interface Options {
to: string,
subject: string,
text: string,
}
const sendEmail = (options: Options) => {
const transporter = nodemailer.createTransport({
host:process.env.EMAIL_HOST,
port:Number(process.env.EMAIL_PORT),
auth: {
user:process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
},
});
const mailOptions = {
from: process.env.EMAIL_FROM,
to: options.to,
subject: options.subject,
html: options.text
}
transporter.sendMail(mailOptions, function (err, info) {
if (err) {
console.log(err);
} else {
console.log(info);
}
})
}
module.exports = sendEmail;
Top comments (2)
Great article Osiroski!
I have so many issues with your project. Please contract me if you're available.