init: init project
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import "dotenv/config";
|
||||
|
||||
export const configs = {
|
||||
port: process.env.PORT,
|
||||
env: process.env.NODE_ENV,
|
||||
db_url: process.env.DATABASE_URL,
|
||||
jwt: {
|
||||
access_token: process.env.ACCESS_TOKEN,
|
||||
refresh_token: process.env.REFRESH_TOKEN,
|
||||
access_expires: process.env.ACCESS_EXPIRES,
|
||||
refresh_expires: process.env.REFRESH_EXPIRES,
|
||||
reset_secret: process.env.RESET_SECRET,
|
||||
reset_expires: process.env.RESET_EXPIRES,
|
||||
front_end_url: process.env.FRONT_END_URL,
|
||||
verified_token: process.env.VERIFIED_TOKEN,
|
||||
},
|
||||
email: {
|
||||
app_email: process.env.APP_USER_EMAIL,
|
||||
app_password: process.env.APP_PASSWORD,
|
||||
},
|
||||
cloudinary: {
|
||||
cloud_name: process.env.CLOUD_NAME,
|
||||
cloud_api_key: process.env.CLOUD_API_KEY,
|
||||
cloud_api_secret: process.env.CLOUD_API_SECRET,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ZodError, ZodIssue } from 'zod'
|
||||
import { TErrorSources, TGenericErrorResponse } from '../types/error'
|
||||
|
||||
const handleZodError = (err: ZodError): TGenericErrorResponse => {
|
||||
const errorSources: TErrorSources = err.issues.map((issue: ZodIssue) => {
|
||||
return {
|
||||
path: issue?.path[issue.path.length - 1] as string,
|
||||
message: issue.message
|
||||
}
|
||||
})
|
||||
|
||||
const statusCode = 400
|
||||
return {
|
||||
statusCode,
|
||||
message: 'Validation Error',
|
||||
errorSources
|
||||
}
|
||||
}
|
||||
|
||||
export default handleZodError
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import "dotenv/config";
|
||||
import { PrismaClient } from "../../../prisma/generated/prisma/client";
|
||||
|
||||
const connectionString = `${process.env.DATABASE_URL}`;
|
||||
|
||||
const adapter = new PrismaPg({ connectionString });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
export { prisma };
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { configs } from "../configs";
|
||||
import { AppError } from "../utils/app_error";
|
||||
import { jwtHelpers, JwtPayloadType } from "../utils/JWT";
|
||||
|
||||
type Role = "ADMIN" | "USER";
|
||||
|
||||
const auth = (...roles: Role[]) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const token = req.headers.authorization || req.cookies.access_token;
|
||||
if (!token) {
|
||||
throw new AppError("You are not authorize!!", 401);
|
||||
}
|
||||
const verifiedUser = jwtHelpers.verifyToken(
|
||||
token,
|
||||
configs.jwt.access_token as string,
|
||||
);
|
||||
if (!roles.length || !roles.includes(verifiedUser.role)) {
|
||||
throw new AppError("You are not authorize!!", 401);
|
||||
}
|
||||
req.user = verifiedUser as JwtPayloadType;
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default auth;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ErrorRequestHandler } from "express";
|
||||
import { ZodError } from "zod";
|
||||
import { configs } from "../configs";
|
||||
import handleZodError from "../errors/zodError";
|
||||
import { TErrorSources } from "../types/error";
|
||||
import { AppError } from "../utils/app_error";
|
||||
|
||||
const globalErrorHandler: ErrorRequestHandler = (err, req, res, next) => {
|
||||
let statusCode = 500;
|
||||
let message = "Something went wrong!";
|
||||
let errorSources: TErrorSources = [
|
||||
{
|
||||
path: "",
|
||||
message: "Something went wrong",
|
||||
},
|
||||
];
|
||||
|
||||
if (err instanceof ZodError) {
|
||||
const simplifiedError = handleZodError(err);
|
||||
statusCode = simplifiedError?.statusCode;
|
||||
message = simplifiedError?.message;
|
||||
errorSources = simplifiedError?.errorSources;
|
||||
} else if (err instanceof AppError) {
|
||||
statusCode = err?.statusCode;
|
||||
message = err.message;
|
||||
errorSources = [
|
||||
{
|
||||
path: "",
|
||||
message: err?.message,
|
||||
},
|
||||
];
|
||||
} else if (err instanceof Error) {
|
||||
message = err.message;
|
||||
errorSources = [
|
||||
{
|
||||
path: "",
|
||||
message: err?.message,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
message,
|
||||
errorSources,
|
||||
err,
|
||||
stack: configs.env === "development" ? err?.stack : null,
|
||||
});
|
||||
};
|
||||
|
||||
export default globalErrorHandler;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
const notFound = (req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(404).json({
|
||||
message: 'Sorry Route is not found!! 😴😴😴',
|
||||
success: false,
|
||||
error: '',
|
||||
});
|
||||
};
|
||||
export default notFound;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
|
||||
const RequestValidator = (schema: any) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
req.body = await schema.parseAsync(req.body);
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default RequestValidator;
|
||||
@@ -0,0 +1,16 @@
|
||||
import multer from "multer";
|
||||
import path from "path";
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, path.join(process.cwd(), "uploads"))
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
|
||||
cb(null, file.originalname)
|
||||
}
|
||||
})
|
||||
|
||||
const uploader = multer({ storage: storage })
|
||||
|
||||
export default uploader;
|
||||
@@ -0,0 +1,116 @@
|
||||
import { configs } from "../../configs";
|
||||
import catchAsync from "../../utils/catch_async";
|
||||
import manageResponse from "../../utils/manage_response";
|
||||
import { account_services } from "./account.service";
|
||||
|
||||
const create_account = catchAsync(async (req, res) => {
|
||||
const result = await account_services.create_account_into_db(req);
|
||||
manageResponse(res, {
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
message: "Account created successfully",
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
const verify_account_using_otp = catchAsync(async (req, res) => {
|
||||
const result = await account_services.verify_account_using_otp_into_db(req);
|
||||
manageResponse(res, {
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
message: "Otp verification successfull",
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
const verify_account_using_link = catchAsync(async (req, res) => {
|
||||
const result = await account_services.verify_account_using_link_into_db(req);
|
||||
manageResponse(res, {
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
message: "Account verification successfull",
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
const login_user = catchAsync(async (req, res) => {
|
||||
const result = await account_services.login_user_into_db(req);
|
||||
|
||||
// set access token into cookie
|
||||
res.cookie("access_token", result, {
|
||||
secure: configs.env === "production",
|
||||
httpOnly: true,
|
||||
});
|
||||
|
||||
manageResponse(res, {
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
message: "User logged in successfully",
|
||||
data: {
|
||||
accessToken: result,
|
||||
},
|
||||
});
|
||||
});
|
||||
const get_user_account = catchAsync(async (req, res) => {
|
||||
const result = await account_services.get_user_account_from_db(req);
|
||||
|
||||
manageResponse(res, {
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
message: "Account fetched successfully",
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
const change_password = catchAsync(async (req, res) => {
|
||||
const result = await account_services.change_password_into_db(req);
|
||||
manageResponse(res, {
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
message: "Password Change successfully",
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
const resend_otp_and_verification_link = catchAsync(async (req, res) => {
|
||||
const result =
|
||||
await account_services.resend_otp_and_verification_link_from_db(req);
|
||||
manageResponse(res, {
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
message: "OTP reset successfully",
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
const forget_password_genereate_reset_token = catchAsync(async (req, res) => {
|
||||
const result =
|
||||
await account_services.forget_password_genereate_reset_token_from_db(req);
|
||||
manageResponse(res, {
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
message: "Password reset successfully",
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
const reset_password_using_token = catchAsync(async (req, res) => {
|
||||
const result = await account_services.reset_password_using_token_into_db(req);
|
||||
manageResponse(res, {
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
message: "Password reset successfully",
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
export const account_controller = {
|
||||
create_account,
|
||||
login_user,
|
||||
get_user_account,
|
||||
change_password,
|
||||
verify_account_using_otp,
|
||||
resend_otp_and_verification_link,
|
||||
verify_account_using_link,
|
||||
forget_password_genereate_reset_token,
|
||||
reset_password_using_token
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Router } from "express";
|
||||
import auth from "../../middlewares/auth";
|
||||
import RequestValidator from "../../middlewares/request_validator";
|
||||
import { account_controller } from "./account.controller";
|
||||
import { account_validation } from "./account.validation";
|
||||
|
||||
const accountRouter = Router();
|
||||
|
||||
accountRouter.post(
|
||||
"/sign-up",
|
||||
RequestValidator(account_validation.sign_up),
|
||||
account_controller.create_account,
|
||||
);
|
||||
accountRouter.post(
|
||||
"/sign-in",
|
||||
RequestValidator(account_validation.sing_in),
|
||||
account_controller.login_user,
|
||||
);
|
||||
accountRouter.put(
|
||||
"/verify-otp",
|
||||
RequestValidator(account_validation.verify_otp),
|
||||
account_controller.verify_account_using_otp,
|
||||
);
|
||||
accountRouter.put(
|
||||
"/verify-link",
|
||||
RequestValidator(account_validation.verify_link),
|
||||
account_controller.verify_account_using_link,
|
||||
);
|
||||
accountRouter.get(
|
||||
"/me",
|
||||
auth("USER", "ADMIN"),
|
||||
account_controller.get_user_account,
|
||||
);
|
||||
accountRouter.put(
|
||||
"/change-password",
|
||||
auth("USER", "ADMIN"),
|
||||
RequestValidator(account_validation.change_password),
|
||||
account_controller.change_password,
|
||||
);
|
||||
accountRouter.put(
|
||||
"/resend-otp",
|
||||
RequestValidator(account_validation.resend_otp),
|
||||
account_controller.resend_otp_and_verification_link,
|
||||
);
|
||||
accountRouter.put(
|
||||
"/forget-password",
|
||||
RequestValidator(account_validation.resend_otp),
|
||||
account_controller.forget_password_genereate_reset_token,
|
||||
);
|
||||
accountRouter.put(
|
||||
"/reset-password",
|
||||
RequestValidator(account_validation.reset_pass),
|
||||
account_controller.reset_password_using_token,
|
||||
);
|
||||
export default accountRouter;
|
||||
@@ -0,0 +1,402 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { Request } from "express";
|
||||
import { configs } from "../../configs";
|
||||
import { prisma } from "../../lib/prisma";
|
||||
import { AppError } from "../../utils/app_error";
|
||||
import { jwtHelpers } from "../../utils/JWT";
|
||||
import { otpGenerator } from "../../utils/otpGenerator";
|
||||
import sendMail from "../../utils/mail_sender";
|
||||
|
||||
const create_account_into_db = async (req: Request) => {
|
||||
const payload = req?.body;
|
||||
|
||||
// hash password
|
||||
const hashPassword = bcrypt.hashSync(payload.password, 10);
|
||||
|
||||
// create account and profile
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const account = await tx.account.create({
|
||||
data: {
|
||||
email: payload.email,
|
||||
password: hashPassword,
|
||||
},
|
||||
});
|
||||
|
||||
const profile = await tx.profile.create({
|
||||
data: {
|
||||
fullName: payload.fullName,
|
||||
accountId: account.id,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
account,
|
||||
profile,
|
||||
};
|
||||
});
|
||||
|
||||
// sending otp and verification link
|
||||
const newOtp = otpGenerator();
|
||||
const verificationToken = jwtHelpers.generateToken(
|
||||
{
|
||||
email: payload.email,
|
||||
accountId: result.account.id,
|
||||
},
|
||||
configs.jwt.verified_token as string,
|
||||
"5m",
|
||||
);
|
||||
const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`;
|
||||
// save otp into db
|
||||
await prisma.account.update({
|
||||
where: {
|
||||
email: payload.email,
|
||||
},
|
||||
data: {
|
||||
lastOtp: newOtp,
|
||||
lastOtpSendingTime: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await sendMail({
|
||||
to: payload.email as string,
|
||||
subject: "welcome to - Please verify your account",
|
||||
htmlBody: `
|
||||
<p><strong>OTP</strong> ${newOtp}</p>
|
||||
<small>Otp will be expire in 5 minutes</small>
|
||||
|
||||
<br/> <br/>
|
||||
|
||||
<p>Or you can use Verification link </p>
|
||||
<p>${verificationLink}</p>
|
||||
`,
|
||||
textBody: "You can use otp or direct link",
|
||||
name: payload.fullName,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const verify_account_using_otp_into_db = async (req: Request) => {
|
||||
const payload: { email: string; otp: string } = req?.body;
|
||||
// check account
|
||||
const account = await prisma.account.findUnique({
|
||||
where: {
|
||||
email: payload.email,
|
||||
},
|
||||
});
|
||||
// check if account exists
|
||||
if (!account) {
|
||||
throw new AppError("Account not found", 404);
|
||||
}
|
||||
// match with last otp
|
||||
const isOtpMatch = payload.otp === account.lastOtp;
|
||||
if (!isOtpMatch) {
|
||||
throw new AppError("Invalid OTP, Please try again!!", 401);
|
||||
}
|
||||
// check otp timing
|
||||
const OTP_EXPIRY_TIME = 5 * 60 * 1000; // 5 minutes in ms
|
||||
const isOtpExpired = account.lastOtpSendingTime
|
||||
? new Date().getTime() - new Date(account.lastOtpSendingTime).getTime() >
|
||||
OTP_EXPIRY_TIME
|
||||
: true;
|
||||
|
||||
if (isOtpExpired) {
|
||||
throw new AppError("OTP Expired, Please try again!!", 401);
|
||||
}
|
||||
// change account status
|
||||
await prisma.account.update({
|
||||
where: {
|
||||
id: account.id,
|
||||
},
|
||||
data: {
|
||||
isAccountVerified: true,
|
||||
},
|
||||
});
|
||||
// infuter user welcome email
|
||||
return "";
|
||||
};
|
||||
|
||||
const verify_account_using_link_into_db = async (req: Request) => {
|
||||
const token = req?.body?.token as string;
|
||||
let decoadeToken: any;
|
||||
try {
|
||||
decoadeToken = jwtHelpers.verifyToken(
|
||||
token,
|
||||
configs.jwt.verified_token as string,
|
||||
);
|
||||
} catch (error: any) {
|
||||
if (error?.message == "invalid signature") {
|
||||
throw new AppError("Invalid Token", 403);
|
||||
} else if (error?.message == "jwt expired") {
|
||||
throw new AppError("Token expired, please reset again", 403);
|
||||
}
|
||||
}
|
||||
// check account
|
||||
const account = await prisma.account.findUnique({
|
||||
where: {
|
||||
email: decoadeToken.email,
|
||||
},
|
||||
});
|
||||
// check if account exists
|
||||
if (!account) {
|
||||
throw new AppError("Account not found", 404);
|
||||
}
|
||||
|
||||
// change account status
|
||||
await prisma.account.update({
|
||||
where: {
|
||||
id: account.id,
|
||||
},
|
||||
data: {
|
||||
isAccountVerified: true,
|
||||
},
|
||||
});
|
||||
// infuter user welcome email
|
||||
return "";
|
||||
};
|
||||
|
||||
const login_user_into_db = async (req: Request) => {
|
||||
const payload = req?.body;
|
||||
const account = await prisma.account.findUnique({
|
||||
where: {
|
||||
email: payload.email,
|
||||
},
|
||||
});
|
||||
|
||||
// check if account exists
|
||||
if (!account) {
|
||||
throw new AppError("Account not found", 404);
|
||||
}
|
||||
|
||||
// checking password
|
||||
const isPasswordMatch = bcrypt.compareSync(
|
||||
payload.password,
|
||||
account.password,
|
||||
);
|
||||
|
||||
if (!isPasswordMatch) {
|
||||
throw new AppError("Invalid password", 401);
|
||||
}
|
||||
|
||||
// check if account is deleted
|
||||
if (account.isDeleted) {
|
||||
throw new AppError("Account is deleted", 401);
|
||||
}
|
||||
// check if account is verified
|
||||
if (!account.isAccountVerified) {
|
||||
throw new AppError("Account is not verified", 401);
|
||||
}
|
||||
|
||||
// generate access
|
||||
const accessToken = jwtHelpers.generateToken(
|
||||
{
|
||||
email: account.email,
|
||||
role: account.role,
|
||||
accountId: account.id,
|
||||
},
|
||||
configs.jwt.access_token as string,
|
||||
configs.jwt.access_expires as string,
|
||||
);
|
||||
return accessToken;
|
||||
};
|
||||
|
||||
const get_user_account_from_db = async (req: Request) => {
|
||||
const user = req?.user;
|
||||
|
||||
const result = await prisma.account.findUnique({
|
||||
where: {
|
||||
id: user?.accountId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
role: true,
|
||||
isAccountVerified: true,
|
||||
isDeleted: true,
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const change_password_into_db = async (req: Request) => {
|
||||
const user = req?.user;
|
||||
// payload
|
||||
const payload: { oldPassword: string; newPassword: string } = req?.body;
|
||||
// check old and new password is not same
|
||||
const isSamePassword = payload.oldPassword === payload.newPassword;
|
||||
if (isSamePassword) {
|
||||
throw new AppError(
|
||||
"Old and new password are same, Please provide deffirent password",
|
||||
404,
|
||||
);
|
||||
}
|
||||
// check user validity
|
||||
const isUserExist = await prisma.account.findFirst({
|
||||
where: {
|
||||
email: user?.email,
|
||||
},
|
||||
});
|
||||
// if account not exists
|
||||
if (!isUserExist) {
|
||||
throw new AppError("Account not found!!", 404);
|
||||
}
|
||||
// check old password
|
||||
const isPasswordMatch = bcrypt.compareSync(
|
||||
payload.oldPassword,
|
||||
isUserExist.password,
|
||||
);
|
||||
if (!isPasswordMatch) {
|
||||
throw new AppError("Incorrect password", 401);
|
||||
}
|
||||
// change password logic
|
||||
const newHashPassword = bcrypt.hashSync(payload.newPassword, 10);
|
||||
await prisma.account.update({
|
||||
where: {
|
||||
id: isUserExist.id,
|
||||
},
|
||||
data: {
|
||||
password: newHashPassword,
|
||||
},
|
||||
});
|
||||
|
||||
// in future email notification for more sucurity
|
||||
return "";
|
||||
};
|
||||
|
||||
const resend_otp_and_verification_link_from_db = async (req: Request) => {
|
||||
const email = req?.body?.email as string;
|
||||
const account = await prisma.account.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
// check if account exists
|
||||
if (!account) {
|
||||
throw new AppError("Account not found", 404);
|
||||
}
|
||||
// if already verified
|
||||
if (account.isAccountVerified) {
|
||||
throw new AppError("Account already verified", 403);
|
||||
}
|
||||
// make new otp and verification link
|
||||
const newOtp = otpGenerator();
|
||||
const verificationToken = jwtHelpers.generateToken(
|
||||
{
|
||||
email,
|
||||
accountId: account.id,
|
||||
},
|
||||
configs.jwt.verified_token as string,
|
||||
"5m",
|
||||
);
|
||||
const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`;
|
||||
// save otp into db
|
||||
await prisma.account.update({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
data: {
|
||||
lastOtp: newOtp,
|
||||
lastOtpSendingTime: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await sendMail({
|
||||
to: email as string,
|
||||
subject: "New Verification otp and link",
|
||||
htmlBody: `
|
||||
<p><strong>OTP</strong> ${newOtp}</p>
|
||||
<small>Otp will be expire in 5 minutes</small>
|
||||
|
||||
<br/> <br/>
|
||||
|
||||
<p>Or you can use Verification link </p>
|
||||
<p>${verificationLink}</p>
|
||||
`,
|
||||
textBody: "You can use otp or direct link",
|
||||
});
|
||||
};
|
||||
|
||||
const forget_password_genereate_reset_token_from_db = async (req: Request) => {
|
||||
const email = req?.body?.email as string;
|
||||
const account = await prisma.account.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
// check if account exists
|
||||
if (!account) {
|
||||
throw new AppError("Account not found", 404);
|
||||
}
|
||||
// generate forget token
|
||||
const verificationToken = jwtHelpers.generateToken(
|
||||
{
|
||||
email: email,
|
||||
accountId: account.id,
|
||||
},
|
||||
configs.jwt.verified_token as string,
|
||||
"5m",
|
||||
);
|
||||
const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`;
|
||||
|
||||
await sendMail({
|
||||
to: email as string,
|
||||
subject: "Forget Password- Use this link for new password ",
|
||||
htmlBody: `
|
||||
<p>Your Reset Link: </p>
|
||||
<p>${verificationLink}</p>
|
||||
`,
|
||||
textBody: "",
|
||||
});
|
||||
};
|
||||
|
||||
const reset_password_using_token_into_db = async (req: Request) => {
|
||||
const token = req?.body?.token as string;
|
||||
const newPass = req?.body?.newPass as string;
|
||||
let decoadeToken: any;
|
||||
try {
|
||||
decoadeToken = jwtHelpers.verifyToken(
|
||||
token,
|
||||
configs.jwt.verified_token as string,
|
||||
);
|
||||
} catch (error: any) {
|
||||
if (error?.message == "invalid signature") {
|
||||
throw new AppError("Invalid Token", 403);
|
||||
} else if (error?.message == "jwt expired") {
|
||||
throw new AppError("Link expired, please reset again", 403);
|
||||
}
|
||||
}
|
||||
// check account
|
||||
const account = await prisma.account.findUnique({
|
||||
where: {
|
||||
email: decoadeToken.email,
|
||||
},
|
||||
});
|
||||
// check if account exists
|
||||
if (!account) {
|
||||
throw new AppError("Account not found", 404);
|
||||
}
|
||||
|
||||
// change account password
|
||||
const newHash = bcrypt.hashSync(newPass, 10);
|
||||
await prisma.account.update({
|
||||
where: {
|
||||
id: account.id,
|
||||
},
|
||||
data: {
|
||||
password: newHash,
|
||||
},
|
||||
});
|
||||
// infuter user alart for changing password
|
||||
return "";
|
||||
};
|
||||
export const account_services = {
|
||||
create_account_into_db,
|
||||
login_user_into_db,
|
||||
get_user_account_from_db,
|
||||
change_password_into_db,
|
||||
verify_account_using_otp_into_db,
|
||||
resend_otp_and_verification_link_from_db,
|
||||
verify_account_using_link_into_db,
|
||||
forget_password_genereate_reset_token_from_db,
|
||||
reset_password_using_token_into_db
|
||||
};
|
||||
@@ -0,0 +1,187 @@
|
||||
export const accountSwaggerDocs = {
|
||||
"/api/auth/sign-up": {
|
||||
post: {
|
||||
tags: ["account"],
|
||||
summary: "Create new account",
|
||||
description: "",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({
|
||||
email: "user@gmail.com",
|
||||
password: "password",
|
||||
fullName: "User",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "account created successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/sign-in": {
|
||||
post: {
|
||||
tags: ["account"],
|
||||
summary: "Sign In your account",
|
||||
description: "",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({
|
||||
email: "user@gmail.com",
|
||||
password: "password",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "User signed in successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/verify-otp": {
|
||||
put: {
|
||||
tags: ["account"],
|
||||
summary: "Verify OTP",
|
||||
description: "",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({
|
||||
email: "user@gmail.com",
|
||||
otp: "654321",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "OTP verification successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/verify-link": {
|
||||
put: {
|
||||
tags: ["account"],
|
||||
summary: "Verify Link",
|
||||
description: "",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({
|
||||
token: "dsakfjasdkj",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "Token verification successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/me": {
|
||||
get: {
|
||||
tags: ["account"],
|
||||
summary: "Get me account",
|
||||
description: "",
|
||||
responses: {
|
||||
200: { description: "account fetched successfully" },
|
||||
401: { description: "unauthorized" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/change-password": {
|
||||
put: {
|
||||
tags: ["account"],
|
||||
summary: "Change Password",
|
||||
description: "",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({
|
||||
oldPassword: "123456",
|
||||
newPassword: "654321",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "Passwrod change successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/resend-otp": {
|
||||
put: {
|
||||
tags: ["account"],
|
||||
summary: "Resend OTP",
|
||||
description: "",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({
|
||||
email: "user@gmail.com",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "OTP resend successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/forget-password": {
|
||||
put: {
|
||||
tags: ["account"],
|
||||
summary: "Forget Password",
|
||||
description: "",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({
|
||||
email: "user@gmail.com",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "Forget password successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/reset-password": {
|
||||
put: {
|
||||
tags: ["account"],
|
||||
summary: "Reset Password",
|
||||
description: "",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
example: JSON.stringify({
|
||||
token: "dkfjadskfds",
|
||||
newPass: "newpass",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "Password reset successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import z from "zod";
|
||||
|
||||
const sign_up = z.object({
|
||||
email: z.string("Email is required."),
|
||||
password: z.string("Password is required."),
|
||||
fullName: z.string("Full name is required."),
|
||||
});
|
||||
|
||||
const sing_in = z.object({
|
||||
email: z.string("Email is required."),
|
||||
password: z.string("Password is required."),
|
||||
});
|
||||
|
||||
const change_password = z.object({
|
||||
oldPassword: z.string("Old Password is requied"),
|
||||
newPassword: z.string("New Password is required"),
|
||||
});
|
||||
|
||||
const verify_otp = z.object({
|
||||
email: z.string("Email is requied"),
|
||||
otp: z.string("OTP is required"),
|
||||
});
|
||||
const verify_link = z.object({
|
||||
token: z.string("Token is required "),
|
||||
});
|
||||
const resend_otp = z.object({
|
||||
email: z.string("Email is requied"),
|
||||
});
|
||||
const reset_pass = z.object({
|
||||
token: z.string("Token is required"),
|
||||
newPass: z.string("Password is required"),
|
||||
});
|
||||
export const account_validation = {
|
||||
sign_up,
|
||||
sing_in,
|
||||
change_password,
|
||||
verify_otp,
|
||||
resend_otp,
|
||||
verify_link,
|
||||
reset_pass
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import catchAsync from "../../utils/catch_async";
|
||||
import manageResponse from "../../utils/manage_response";
|
||||
import { profile_service } from "./profile.service";
|
||||
|
||||
const update_profile = catchAsync(async (req, res) => {
|
||||
const result = await profile_service.update_profile_into_db(req);
|
||||
manageResponse(res, {
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
message: "profile updated successfully.",
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
|
||||
export const profile_controller = {
|
||||
update_profile,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Router } from "express";
|
||||
import RequestValidator from "../../middlewares/request_validator";
|
||||
import { profile_controller } from "./profile.controller";
|
||||
import { profile_validations } from "./profile.validation";
|
||||
import auth from "../../middlewares/auth";
|
||||
import uploader from "../../middlewares/uploader";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.patch(
|
||||
"/",
|
||||
auth("USER"),
|
||||
uploader.single("file"),
|
||||
(req, res, next) => {
|
||||
req.body = JSON.parse(req?.body?.data);
|
||||
next();
|
||||
},
|
||||
RequestValidator(profile_validations.update_profile),
|
||||
profile_controller.update_profile,
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Request } from "express";
|
||||
import uploadCloud from "../../utils/cloudinary";
|
||||
import { prisma } from "../../lib/prisma";
|
||||
import { JwtPayloadType } from "../../utils/JWT";
|
||||
|
||||
const update_profile_into_db = async (req: Request) => {
|
||||
const user = req?.user as JwtPayloadType;
|
||||
const payload = req?.body;
|
||||
const file = req?.file;
|
||||
// check file and upload to cloud
|
||||
if (file) {
|
||||
const cloudRes = await uploadCloud(file);
|
||||
payload.profilePhoto = cloudRes?.secure_url;
|
||||
}
|
||||
const result = await prisma.profile.update({
|
||||
where: {
|
||||
accountId: user.accountId as string,
|
||||
},
|
||||
data: payload,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export const profile_service = {
|
||||
update_profile_into_db,
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
export const profileSwaggerDocs = {
|
||||
"/api/profile": {
|
||||
patch: {
|
||||
tags: ["profile"],
|
||||
summary: "Update profile",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"multipart/form-data": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fullName: { type: "string" },
|
||||
},
|
||||
},
|
||||
file: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: { description: "profile updated successfully" },
|
||||
500: { description: "Validation error or internal server error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
const update_profile = z.object({
|
||||
fullName: z.string().optional(),
|
||||
});
|
||||
|
||||
export const profile_validations = {
|
||||
update_profile,
|
||||
};
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
export type TErrorSources = {
|
||||
path: string | number
|
||||
message: string
|
||||
}[]
|
||||
|
||||
export type TGenericErrorResponse = {
|
||||
statusCode: number
|
||||
message: string
|
||||
errorSources: TErrorSources
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import { TJwtUser } from "../modules/auth/auth.interface";
|
||||
import { JwtPayloadType } from "../utils/JWT";
|
||||
|
||||
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: JwtPayloadType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import jwt, { JwtPayload, Secret, SignOptions } from "jsonwebtoken";
|
||||
|
||||
const generateToken = (payload: object, secret: Secret, expiresIn: string) => {
|
||||
const token = jwt.sign(payload, secret, {
|
||||
algorithm: "HS256",
|
||||
expiresIn: expiresIn,
|
||||
} as SignOptions);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
const verifyToken = (token: string, secret: Secret): JwtPayload => {
|
||||
return jwt.verify(token, secret) as JwtPayload;
|
||||
};
|
||||
|
||||
export const jwtHelpers = {
|
||||
generateToken,
|
||||
verifyToken,
|
||||
};
|
||||
export type JwtPayloadType = JwtPayload & {
|
||||
email: string;
|
||||
role: string;
|
||||
accountId: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
};
|
||||
export type JwtTokenType = string | JwtPayloadType | null;
|
||||
@@ -0,0 +1,12 @@
|
||||
export class AppError extends Error {
|
||||
public statusCode: number;
|
||||
constructor(message: string, statusCode: number, stack = '') {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
if (stack) {
|
||||
this.stack = stack;
|
||||
} else {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { RequestHandler, Request, Response, NextFunction } from 'express';
|
||||
|
||||
const catchAsync = (fn: RequestHandler): RequestHandler => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await fn(req, res, next);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default catchAsync;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { v2 as cloudinary } from 'cloudinary';
|
||||
import fs from 'fs';
|
||||
import { configs } from '../configs';
|
||||
|
||||
type ICloudinaryResponse = {
|
||||
asset_id: string;
|
||||
public_id: string;
|
||||
version: number;
|
||||
version_id: string;
|
||||
signature: string;
|
||||
width: number;
|
||||
height: number;
|
||||
format: string;
|
||||
resource_type: string;
|
||||
created_at: string;
|
||||
tags: string[];
|
||||
bytes: number;
|
||||
type: string;
|
||||
etag: string;
|
||||
placeholder: boolean;
|
||||
url: string;
|
||||
secure_url: string;
|
||||
folder: string;
|
||||
overwritten: boolean;
|
||||
original_filename: string;
|
||||
original_extension: string;
|
||||
api_key: string;
|
||||
};
|
||||
|
||||
type IFile = {
|
||||
fieldname: string;
|
||||
originalname: string;
|
||||
encoding: string;
|
||||
mimetype: string;
|
||||
destination: string;
|
||||
filename: string;
|
||||
path: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
// Configuration
|
||||
cloudinary.config({
|
||||
cloud_name: configs.cloudinary.cloud_name!,
|
||||
api_key: configs.cloudinary.cloud_api_key!,
|
||||
api_secret: configs.cloudinary.cloud_api_secret!,
|
||||
});
|
||||
|
||||
const uploadCloud = async (
|
||||
file: IFile
|
||||
): Promise<ICloudinaryResponse | undefined> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cloudinary.uploader.upload(
|
||||
file.path,
|
||||
(error: Error, result: ICloudinaryResponse) => {
|
||||
fs.unlinkSync(file.path);
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default uploadCloud;
|
||||
@@ -0,0 +1,102 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { configs } from '../configs';
|
||||
type TMailContent = {
|
||||
to: string,
|
||||
subject: string,
|
||||
textBody: string,
|
||||
htmlBody: string,
|
||||
name?: string
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: "smtp.gmail.com",
|
||||
port: 465,
|
||||
secure: true, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: configs.email.app_email!,
|
||||
pass: configs.email.app_password!,
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ Email Sender Function
|
||||
const sendMail = async (payload: TMailContent) => {
|
||||
const info = await transporter.sendMail({
|
||||
from: 'info@digitalcreditai.com',
|
||||
to: payload.to,
|
||||
subject: payload.subject,
|
||||
text: payload.textBody,
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Welcome Email</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Fallback styles for unsupported clients (some email clients ignore <style> tags) */
|
||||
@media only screen and (max-width: 600px) {
|
||||
.container {
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 18px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
style="margin: 0; padding: 0; font-family: Arial, sans-serif;">
|
||||
|
||||
<div style="max-width: 600px; margin: 40px auto; background-color: #f4f4f4; padding: 40px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);"
|
||||
class="container">
|
||||
|
||||
<div style="font-size: 16px; color: #555555; line-height: 1.6;">
|
||||
<p style="margin-bottom: 30px;">Hi <strong>${payload?.name || ""}</strong>,</p>
|
||||
|
||||
${payload?.htmlBody}
|
||||
|
||||
<div
|
||||
style=" margin-top: 60px; text-align: center;">
|
||||
|
||||
<img style="width: 50px; height: 50px; border-radius: 50%;"
|
||||
src="https://imgs.search.brave.com/IZoN38NQxnIIuB1I9E70bW6q5OvbEtz68YaxTe1j-o0/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly9lbGVt/ZW50cy1yZXNpemVk/LmVudmF0b3VzZXJj/b250ZW50LmNvbS9l/bGVtZW50cy1jb3Zl/ci1pbWFnZXMvMjhi/NmVjMTQtMGMwOS00/NGY1LWE5NGUtNmIy/OTM5NTZkMDM2P3c9/NDMzJmNmX2ZpdD1z/Y2FsZS1kb3duJnE9/ODUmZm9ybWF0PWF1/dG8mcz04Mjc0OWYy/ZDUyMmJiM2NlMjNi/OWNhNjhlZmFhNjdk/MTg5OGI4NWIwNzBh/MjQ1NjM4NmI1ZmFj/NWVmNmM5ZTNl"
|
||||
alt="">
|
||||
|
||||
<p style="font-size: 12px;">The Support Team</p>
|
||||
<h3>Company Name</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: #999999; margin-top: 20px; margin-bottom: 10px; text-align: center;">
|
||||
This is an automated message — please do not reply to this email.
|
||||
<br>
|
||||
If you need assistance, feel free to contact our support team.
|
||||
<br><br>
|
||||
Thank you for choosing us!
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
<div style="text-align: center; font-size: 12px; color: #999999; margin-top: 20px;">
|
||||
© {{year}} Your Company. All rights reserved.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
`,
|
||||
});
|
||||
return info
|
||||
};
|
||||
|
||||
export default sendMail;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Response } from "express"
|
||||
interface IResponse<T> {
|
||||
success: boolean,
|
||||
statusCode: number,
|
||||
message: string,
|
||||
data?: T,
|
||||
meta?: {
|
||||
page?: number,
|
||||
limit?: number,
|
||||
skip?: number,
|
||||
total?: number
|
||||
}
|
||||
}
|
||||
|
||||
const manageResponse = <T>(res: Response, payload: IResponse<T>) => {
|
||||
res.status(payload.statusCode).json({
|
||||
success: payload.success,
|
||||
message: payload.message,
|
||||
data: payload.data || undefined || null,
|
||||
meta: payload.meta || undefined || null
|
||||
})
|
||||
}
|
||||
|
||||
export default manageResponse
|
||||
@@ -0,0 +1,29 @@
|
||||
export const otpGenerator = (): string => {
|
||||
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const lower = "abcdefghijklmnopqrstuvwxyz";
|
||||
const numbers = "0123456789";
|
||||
const symbols = "@#$%&*!?";
|
||||
|
||||
const allChars = upper + lower + numbers + symbols;
|
||||
|
||||
let otp = "";
|
||||
|
||||
// Ensure at least one character from each set
|
||||
otp += upper[Math.floor(Math.random() * upper.length)];
|
||||
otp += lower[Math.floor(Math.random() * lower.length)];
|
||||
otp += numbers[Math.floor(Math.random() * numbers.length)];
|
||||
otp += symbols[Math.floor(Math.random() * symbols.length)];
|
||||
|
||||
// Fill the rest randomly
|
||||
for (let i = otp.length; i < 6; i++) {
|
||||
otp += allChars[Math.floor(Math.random() * allChars.length)];
|
||||
}
|
||||
|
||||
// Shuffle to remove predictable order
|
||||
otp = otp
|
||||
.split("")
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.join("");
|
||||
|
||||
return otp;
|
||||
};
|
||||
Reference in New Issue
Block a user