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: add forgot and reset password #255

Merged
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
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);
authRoute.post("/login", login);
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,
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);

// ---------------------------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;
Loading