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

Feature for enabling product to be created by seller #40

Merged
merged 1 commit into from
May 7, 2024
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
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@
"glob": "^10.3.12",
"jsonwebtoken": "^9.0.2",
"mailgen": "^2.0.28",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.13",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"multer": "^1.4.5-lts.1",
"pg": "^8.11.5",
"pg-hstore": "^2.3.4",
"randomstring": "^1.3.0",
"sequelize": "^6.37.2",
"swagger-ui-express": "^5.0.0",
"uuid": "^9.0.1",
Expand All @@ -62,6 +63,7 @@
"@types/nodemailer": "^6.4.14",
"@types/passport": "^1.0.16",
"@types/passport-google-oauth20": "^2.0.14",
"@types/randomstring": "^1.3.0",
"@types/sequelize": "^4.28.20",
"@types/supertest": "^6.0.2",
"@types/swagger-jsdoc": "^6.0.4",
Expand Down Expand Up @@ -93,4 +95,4 @@
"eslint --fix"
]
}
}
}
66 changes: 45 additions & 21 deletions src/controllers/authController.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { Request, Response, NextFunction } from 'express';
import passport from 'passport';
import jwt from 'jsonwebtoken';
import jwt, { JwtPayload } from 'jsonwebtoken';
import User, { UserAttributes } from '../database/models/user';
import { sendInternalErrorResponse, validateFields } from '../validations';
import logger from '../logs/config';
import { passwordCompare } from '../helpers/encrypt';
import { verifyIfSeller } from '../middlewares/authMiddlewares';
import { createOTPToken, saveOTPDB } from '../middlewares/otpAuthMiddleware';
import { userToken } from '../helpers/token.generator';
import { sendErrorResponse } from '../helpers/helper';

const authenticateViaGoogle = (req: Request, res: Response, next: NextFunction) => {
export const authenticateViaGoogle = (req: Request, res: Response, next: NextFunction) => {
passport.authenticate('google', (err: unknown, user: UserAttributes | null) => {
if (err) {
sendInternalErrorResponse(res, err);
Expand All @@ -29,7 +33,7 @@ const authenticateViaGoogle = (req: Request, res: Response, next: NextFunction)
};

// login function
const login = async (req: Request, res: Response): Promise<void> => {
export const login = async (req: Request, res: Response): Promise<void> => {
try {
const { email, password } = req.body;

Expand All @@ -51,47 +55,67 @@ const login = async (req: Request, res: Response): Promise<void> => {
});

if (!user) {
logger.error('Invalid credentials');
res.status(404).json({ ok: false, message: 'Invalid credentials' });
sendErrorResponse(res, 'invalidCredentials');
return;
}

// Check if user is inactive
if (user.status === 'inactive') {
logger.error('Your account has been blocked. Please contact support.');
res.status(403).json({ ok: false, message: 'Your account has been blocked. Please contact support.' });
sendErrorResponse(res, 'inactiveUser');
return;
}

// Check if user is verified
if (!user.verified) {
logger.error('Your account is not verified. Please verify your account.');
res.status(403).json({ ok: false, message: 'Your account is not verified. Please verify your account.' });
sendErrorResponse(res, 'unverifiedUser');
return;
}

// Verify password
const passwordValid = await passwordCompare(password, user.password);
if (!passwordValid) {
logger.error('Invalid credentials');
res.status(404).json({ ok: false, message: 'Invalid credentials' });
sendErrorResponse(res, 'invalidCredentials');
return;
}

// Authenticate user with jwt
const token = jwt.sign({ id: user.id }, process.env.SECRET_KEY as string, {
expiresIn: process.env.JWT_EXPIRATION as string,
});

res.status(200).json({
ok: true,
token: token,
});
await verifyIfSeller(user, req, res);
} catch (err: any) {
const message = (err as Error).message;
logger.error(message);
sendInternalErrorResponse(res, err);
}
};
// Function to verify OTP
export const verifyOTP = async (req: Request, res: Response) => {
try {
const data = req.user as JwtPayload;
const token = await userToken(data.id);

res.status(200).json({ ok: true, token });
} catch (error) {
logger.error('VerifyOTP Internal Server Error', error);
sendInternalErrorResponse(res, error);
}
};
// Function to create OTP Token, Save it Postgres,
export const sendOTP = async (req: Request, res: Response, email: string) => {
const userInfo = await User.findOne({ where: { email } });
if (userInfo) {
const { id, email, firstName } = userInfo.dataValues;

const token = await createOTPToken(id, email, firstName);

const otpSaved = await saveOTPDB(id, token);

export { login, authenticateViaGoogle };
if (otpSaved) {
/**
* The token used for comparing the received OTP via email with the
* generated token, which contains the user's ID.
*/
const accessToken = jwt.sign({ id, FAEnabled: true }, process.env.SECRET_KEY as string, {
expiresIn: process.env.JWT_EXPIRATION as string,
});
res.status(200).json({ ok: true, token: accessToken });
}
}
};
19 changes: 19 additions & 0 deletions src/controllers/categoriesController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Request, Response } from 'express';
import { Category, CategoryCreationAttributes } from '../database/models/Category';
import logger from '../logs/config';

export const createCategory = async (req: Request, res: Response) => {
try {
const { name, description } = req.body as CategoryCreationAttributes;
await Category.create({
name,
description,
});
res.status(201).json({ ok: true, message: 'New category created successully!' });
} catch (error) {
if (error instanceof Error) {
patrickhag marked this conversation as resolved.
Show resolved Hide resolved
logger.error(error.message);
}
res.status(500).json({ error: 'Failed to create category' });
}
};
71 changes: 71 additions & 0 deletions src/controllers/productsController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Request, Response } from 'express';
import uploadImage from '../helpers/claudinary';
import { sendInternalErrorResponse } from '../validations';
import { Product, ProductAttributes } from '../database/models/Product';
import { Size, SizeAttributes } from '../database/models/Size';

export const createProduct = async (req: Request, res: Response) => {
try {
const { name, description, colors } = req.body as ProductAttributes;
const { categoryId } = req.params;
const seller = (await req.user) as any;
const sellerId = seller.id;

// when products exists
const thisProductExists = await Product.findOne({ where: { name } });

if (thisProductExists) {
return res.status(400).json({
ok: false,
message: 'This Product already exists, You can update the stock levels instead.',
data: thisProductExists,
});
}
// handle images
const productImages = ['asdf', 'asdf', 'asdf', 'asdf'];
const images: unknown = req.files;
if (images instanceof Array && images.length > 3) {
for (const image of images) {
const imageBuffer: Buffer = image.buffer;
patrickhag marked this conversation as resolved.
Show resolved Hide resolved
const url = await uploadImage(imageBuffer);
productImages.push(url);
}
} else {
return res.status(400).json({
message: 'Product should have at least 4 images',
});
}

// create product
await Product.create({
sellerId,
name,
description,
categoryId,
colors,
images: productImages,
});

res.status(201).json({
ok: true,
message: 'Thank you for adding new product in the store!',
patrickhag marked this conversation as resolved.
Show resolved Hide resolved
});
} catch (error) {
sendInternalErrorResponse(res, error);
}
};

export const createSize = async (req: Request, res: Response) => {
try {
const { productId } = req.params;
const { size, price, discount, expiryDate } = req.body as SizeAttributes;
await Size.create({ size, price, discount, expiryDate, productId });
res.status(201).json({
ok: true,
message: 'Product size added successfully',
});
} catch (error) {
sendInternalErrorResponse(res, error);
}
};
22 changes: 22 additions & 0 deletions src/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,25 @@ export const resendVerifyLink = async (req: Request, res: Response) => {
sendInternalErrorResponse(res, error);
}
};
// Function to enable two factor authentication(2FA)
export const enable2FA = async (req: Request, res: Response) => {
try {
const userId = (req.user as User).id;

if (!userId) {
return res.status(404).json({ ok: false, error: 'UserId Not Found' });
}
const user = await User.findByPk(userId);

if (!user) {
return res.status(400).json({ ok: false, error: 'User not found' });
}
user.enable2FA = !user.enable2FA;
await user.save();

res.status(201).json({ ok: true, message: `2FA status toggled to ${user.enable2FA}` });
} catch (error) {
logger.error('Enable 2FA', error);
sendInternalErrorResponse(res, error);
}
};
40 changes: 40 additions & 0 deletions src/database/migrations/20240426195145-create-category.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable @typescript-eslint/no-var-requires */
'use strict';

const sequelize = require('sequelize');

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('categories', {
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: sequelize.UUIDV4,
unique: true,
},
name: {
type: Sequelize.STRING,
allowNull: false,
},
description: {
type: Sequelize.TEXT,
allowNull: true,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.literal('NOW()'),
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
},
async down(queryInterface) {
await queryInterface.dropTable('categories');
},
};
68 changes: 68 additions & 0 deletions src/database/migrations/20240429115230-create-product.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-disable @typescript-eslint/no-var-requires */
'use strict';

const sequelize = require('sequelize');

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('products', {
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: sequelize.UUIDV4,
},
name: {
type: Sequelize.STRING,
allowNull: false,
},
description: {
type: Sequelize.TEXT,
allowNull: false,
},
images: {
type: Sequelize.ARRAY(Sequelize.STRING),
allowNull: false,
},
colors: {
type: Sequelize.ARRAY(Sequelize.STRING),
allowNull: true,
},
sellerId: {
type: Sequelize.UUID,
references: {
model: 'Users',
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
allowNull: false,
},
categoryId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'categories',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.literal('NOW()'),
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
});
},
async down(queryInterface) {
await queryInterface.dropTable('product_sizes');
await queryInterface.dropTable('products');
},
};
Loading
Loading