diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts
index 028bb0cc..5b712885 100644
--- a/src/controllers/AuthController.ts
+++ b/src/controllers/AuthController.ts
@@ -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 };
diff --git a/src/models/password-reset-token.ts b/src/models/password-reset-token.ts
new file mode 100644
index 00000000..96d44e27
--- /dev/null
+++ b/src/models/password-reset-token.ts
@@ -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;
+}
diff --git a/src/models/user.ts b/src/models/user.ts
index 96cc8842..26cfe4b7 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -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"])
@@ -101,4 +102,10 @@ export class User extends ExtendedBaseEntity {
@DeleteDateColumn({ nullable: true })
deletedAt: Date;
+
+ @OneToMany(
+ () => PasswordResetToken,
+ (passwordResetToken) => passwordResetToken.user,
+ )
+ passwordResetTokens: PasswordResetToken[];
}
diff --git a/src/routes/auth.ts b/src/routes/auth.ts
index fedc4c86..5faf76ff 100644
--- a/src/routes/auth.ts
+++ b/src/routes/auth.ts
@@ -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();
@@ -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('Authenticate with Google');
+authRoute.get("/test-google-auth", (req, res) => {
+ res.send(
+ 'Authenticate with Google',
+ );
});
-
/**
* @openapi
* /auth/google:
@@ -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
@@ -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 };
diff --git a/src/services/auth.services.ts b/src/services/auth.services.ts
index a27bf008..f955d5af 100644
--- a/src/services/auth.services.ts
+++ b/src/services/auth.services.ts
@@ -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;
@@ -45,7 +47,7 @@ export class AuthService implements IAuthService {
config.TOKEN_SECRET,
{
expiresIn: "1d",
- }
+ },
);
const mailSent = await Sendmail({
@@ -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);
@@ -96,7 +98,7 @@ export class AuthService implements IAuthService {
}
public async login(
- payload: IUserLogin
+ payload: IUserLogin,
): Promise<{ access_token: string; user: Partial }> {
const { email, password } = payload;
@@ -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);
+ }
+ }
}
diff --git a/src/utils/generate-reset-token.ts b/src/utils/generate-reset-token.ts
new file mode 100644
index 00000000..bf3dd8d4
--- /dev/null
+++ b/src/utils/generate-reset-token.ts
@@ -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;