Skip to content

Commit

Permalink
Merge pull request #255 from AdeGneus/feat/forgot-reset-password
Browse files Browse the repository at this point in the history
feat: add forgot and reset password
  • Loading branch information
incredible-phoenix246 authored Jul 24, 2024
2 parents c2df9b7 + b3d95db commit 1f0feb2
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 20 deletions.
30 changes: 29 additions & 1 deletion src/controllers/AuthController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,32 @@ const login = async (req: Request, res: Response, next: NextFunction) => {
}
};

export { signUp, verifyOtp, login };
const forgotPassword = async (
req: Request,
res: Response,
next: NextFunction,
) => {
try {
const { email } = req.body;
const { message } = await authService.forgotPassword(email);
res.status(200).json({ status: "sucess", status_code: 200, message });
} catch (error) {
next(error);
}
};

const resetPassword = async (
req: Request,
res: Response,
next: NextFunction,
) => {
try {
const { token, newPassword } = req.body;
const { message } = await authService.resetPassword(token, newPassword);
res.status(200).json({ message });
} catch (error) {
next(error);
}
};

export { signUp, verifyOtp, login, forgotPassword, resetPassword };
30 changes: 30 additions & 0 deletions src/models/password-reset-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
import { User } from "./user";

@Entity()
export class PasswordResetToken {
@PrimaryGeneratedColumn("uuid")
id: string;

@Column()
token: string;

@Column()
expiresAt: Date;

@ManyToOne(() => User, (user) => user.passwordResetTokens)
user: User;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
7 changes: 7 additions & 0 deletions src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { getIsInvalidMessage } from "../utils";
import { UserRole } from "../enums/userRoles";
import { Like } from "./like";
import { Payment } from "./payment";
import { PasswordResetToken } from "./password-reset-token";

@Entity()
@Unique(["email"])
Expand Down Expand Up @@ -101,4 +102,10 @@ export class User extends ExtendedBaseEntity {

@DeleteDateColumn({ nullable: true })
deletedAt: Date;

@OneToMany(
() => PasswordResetToken,
(passwordResetToken) => passwordResetToken.user,
)
passwordResetTokens: PasswordResetToken[];
}
40 changes: 25 additions & 15 deletions src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { signUp, verifyOtp, login, changeUserRole } from "../controllers";
import {
signUp,
verifyOtp,
login,
changeUserRole,
forgotPassword,
resetPassword,
} from "../controllers";
import { Router } from "express";
import { authMiddleware, checkPermissions } from "../middleware";
import { UserRole } from "../enums/userRoles";
import { googleAuthCallback, initiateGoogleAuthRequest } from "../controllers/GoogleAuthController";


import {
googleAuthCallback,
initiateGoogleAuthRequest,
} from "../controllers/GoogleAuthController";

const authRoute = Router();

Expand All @@ -13,20 +21,21 @@ authRoute.post("/verify-otp", verifyOtp);
authRoute.post("/login", login);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
authRoute.post("/login", login);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
authRoute.put(
"/api/v1/organizations/:organization_id/users/:user_id/role",
authMiddleware,
checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]),
changeUserRole
);
"/api/v1/organizations/:organization_id/users/:user_id/role",
authMiddleware,

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]),
changeUserRole,
);

// ---------------------------Google Auth Route Begins------------------------- //

// For manually testing google auth functionality locally
authRoute.get('/test-google-auth', (req, res) => {
res.send('<a href="http://localhost:8000/api/v1/auth/google">Authenticate with Google</a>');
authRoute.get("/test-google-auth", (req, res) => {
res.send(
'<a href="http://localhost:8000/api/v1/auth/google">Authenticate with Google</a>',
);
});


/**
* @openapi
* /auth/google:
Expand All @@ -46,8 +55,7 @@ authRoute.get('/test-google-auth', (req, res) => {
* '500':
* description: Internal Server Error
*/
authRoute.get('/google', initiateGoogleAuthRequest);

authRoute.get("/google", initiateGoogleAuthRequest);

/**
* @openapi
Expand Down Expand Up @@ -77,9 +85,11 @@ authRoute.get('/google', initiateGoogleAuthRequest);
* '500':
* description: Internal Server Error - if something goes wrong during the callback handling
*/
authRoute.get('/google/callback', googleAuthCallback);
authRoute.get("/google/callback", googleAuthCallback);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.

// ---------------------------Google Auth Route Ends------------------------- //

authRoute.post("/forgotPassword", forgotPassword);
authRoute.post("/resetPassword", resetPassword);

export { authRoute };
76 changes: 72 additions & 4 deletions src/services/auth.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { Sendmail } from "../utils/mail";
import jwt from "jsonwebtoken";
import { compilerOtp } from "../views/welcome";
import config from "../config";

import generateResetToken from "../utils/generate-reset-token";
import { PasswordResetToken } from "../models/password-reset-token";
import bcrypt from "bcryptjs";
export class AuthService implements IAuthService {
public async signUp(payload: IUserSignUp): Promise<{
mailSent: string;
Expand Down Expand Up @@ -45,7 +47,7 @@ export class AuthService implements IAuthService {
config.TOKEN_SECRET,
{
expiresIn: "1d",
}
},
);

const mailSent = await Sendmail({
Expand All @@ -68,7 +70,7 @@ export class AuthService implements IAuthService {

public async verifyEmail(
token: string,
otp: number
otp: number,
): Promise<{ message: string }> {
try {
const decoded: any = jwt.verify(token, config.TOKEN_SECRET);
Expand Down Expand Up @@ -96,7 +98,7 @@ export class AuthService implements IAuthService {
}

public async login(
payload: IUserLogin
payload: IUserLogin,
): Promise<{ access_token: string; user: Partial<User> }> {
const { email, password } = payload;

Expand Down Expand Up @@ -127,4 +129,70 @@ export class AuthService implements IAuthService {
throw new HttpError(error.status || 500, error.message || error);
}
}

public async forgotPassword(email: string): Promise<{ message: string }> {
try {
const user = await User.findOne({ where: { email } });

if (!user) {
throw new HttpError(404, "User not found");
}

const { resetToken, hashedToken, expiresAt } = generateResetToken();

const passwordResetToken = new PasswordResetToken();
passwordResetToken.token = hashedToken;
passwordResetToken.expiresAt = expiresAt;
passwordResetToken.user = user;

await AppDataSource.manager.save(passwordResetToken);

// Send email
const emailContent = {
from: `Boilerplate <${config.SMTP_USER}>`,
to: email,
subject: "Password Reset",
text: `You requested for a password reset. Use this token to reset your password: ${resetToken}`,
};

await Sendmail(emailContent);

return { message: "Password reset link sent successfully." };
} catch (error) {
throw new HttpError(error.status || 500, error.message || error);
}
}

public async resetPassword(
token: string,
newPassword: string,
): Promise<{ message: string }> {
try {
const passwordResetTokenRepository =
AppDataSource.getRepository(PasswordResetToken);
const passwordResetToken = await passwordResetTokenRepository.findOne({
where: { token },
relations: ["user"],
});

if (!passwordResetToken) {
throw new HttpError(404, "Invalid or expired token");
}

if (passwordResetToken.expiresAt < new Date()) {
throw new HttpError(400, "Token expired");
}

const user = passwordResetToken.user;
const hashedPassword = await bcrypt.hash(newPassword, 10);
user.password = hashedPassword;

await AppDataSource.manager.save(user);
await passwordResetTokenRepository.remove(passwordResetToken);

return { message: "Password reset successfully." };
} catch (error) {
throw new HttpError(error.status || 500, error.message || error);
}
}
}
18 changes: 18 additions & 0 deletions src/utils/generate-reset-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import crypto from "crypto";

const generateResetToken = (): {
resetToken: string;
hashedToken: string;
expiresAt: Date;
} => {
const resetToken = crypto.randomBytes(32).toString("hex");
const hashedToken = crypto
.createHash("sha256")
.update(resetToken)
.digest("hex");
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes from now

return { resetToken, hashedToken, expiresAt };
};

export default generateResetToken;

0 comments on commit 1f0feb2

Please sign in to comment.