From 2a8a5356835dc45bd270a6000a8e56dd88acafd4 Mon Sep 17 00:00:00 2001 From: Ameer jafar Date: Sun, 18 Aug 2024 23:50:10 +0530 Subject: [PATCH 1/6] feat-#3: converted the backend js to ts --- .gitignore | 1 + backend/api/index.js | 2 - backend/api/index.ts | 2 + backend/{app.js => app.ts} | 14 +- backend/config/{db.js => db.ts} | 8 +- backend/config/{passport.js => passport.ts} | 11 +- backend/config/{utils.js => utils.ts} | 0 ...{auth-controller.js => auth-controller.ts} | 92 +++++++++---- ...osts-controller.js => posts-controller.ts} | 47 +++---- ...{user-controller.js => user-controller.ts} | 13 +- .../{eslint.config.js => eslint.config.mjs} | 2 +- backend/middlewares/auth-middleware.js | 30 ---- backend/middlewares/auth-middleware.ts | 50 +++++++ ...rror-middleware.js => error-middleware.ts} | 5 +- ...{post-middleware.js => post-middleware.ts} | 9 +- backend/models/{post.js => post.ts} | 0 backend/models/{user.js => user.ts} | 79 +++++++---- backend/package.json | 22 ++- backend/routes/{auth.js => auth.ts} | 14 +- backend/routes/{posts.js => posts.ts} | 6 +- backend/routes/{user.js => user.ts} | 4 +- backend/server.js | 20 --- backend/server.ts | 24 ++++ backend/services/{redis.js => redis.ts} | 6 +- ...oller.test.js => posts-controller.test.ts} | 14 +- backend/tests/{teardown.js => teardown.ts} | 0 ...oller.test.js => posts-controller.test.ts} | 128 ++++++++---------- backend/tests/utils/helper-objects.js | 25 ---- backend/tests/utils/helper-objects.ts | 51 +++++++ backend/tsconfig.json | 108 +++++++++++++++ backend/types/{role-type.js => role-type.ts} | 0 backend/types/types.d.ts | 8 ++ backend/utils/api-error.js | 20 --- backend/utils/api-error.ts | 37 +++++ backend/utils/api-response.js | 12 -- backend/utils/api-response.ts | 17 +++ backend/utils/async-handler.js | 5 - backend/utils/async-handler.ts | 7 + .../utils/{cache-posts.js => cache-posts.ts} | 10 +- backend/utils/{constants.js => constants.ts} | 0 backend/utils/cookie_options.js | 8 -- backend/utils/cookie_options.ts | 18 +++ backend/utils/middleware.js | 15 -- backend/utils/middleware.ts | 17 +++ 44 files changed, 614 insertions(+), 347 deletions(-) delete mode 100644 backend/api/index.js create mode 100644 backend/api/index.ts rename backend/{app.js => app.ts} (73%) rename backend/config/{db.js => db.ts} (68%) rename backend/config/{passport.js => passport.ts} (80%) rename backend/config/{utils.js => utils.ts} (100%) rename backend/controllers/{auth-controller.js => auth-controller.ts} (69%) rename backend/controllers/{posts-controller.js => posts-controller.ts} (82%) rename backend/controllers/{user-controller.js => user-controller.ts} (82%) rename backend/{eslint.config.js => eslint.config.mjs} (98%) delete mode 100644 backend/middlewares/auth-middleware.js create mode 100644 backend/middlewares/auth-middleware.ts rename backend/middlewares/{error-middleware.js => error-middleware.ts} (71%) rename backend/middlewares/{post-middleware.js => post-middleware.ts} (74%) rename backend/models/{post.js => post.ts} (100%) rename backend/models/{user.js => user.ts} (65%) rename backend/routes/{auth.js => auth.ts} (74%) rename backend/routes/{posts.js => posts.ts} (92%) rename backend/routes/{user.js => user.ts} (88%) delete mode 100644 backend/server.js create mode 100644 backend/server.ts rename backend/services/{redis.js => redis.ts} (83%) rename backend/tests/integration/controllers/{posts-controller.test.js => posts-controller.test.ts} (95%) rename backend/tests/{teardown.js => teardown.ts} (100%) rename backend/tests/unit/controllers/{posts-controller.test.js => posts-controller.test.ts} (77%) delete mode 100644 backend/tests/utils/helper-objects.js create mode 100644 backend/tests/utils/helper-objects.ts create mode 100644 backend/tsconfig.json rename backend/types/{role-type.js => role-type.ts} (100%) create mode 100644 backend/types/types.d.ts delete mode 100644 backend/utils/api-error.js create mode 100644 backend/utils/api-error.ts delete mode 100644 backend/utils/api-response.js create mode 100644 backend/utils/api-response.ts delete mode 100644 backend/utils/async-handler.js create mode 100644 backend/utils/async-handler.ts rename backend/utils/{cache-posts.js => cache-posts.ts} (72%) rename backend/utils/{constants.js => constants.ts} (100%) delete mode 100644 backend/utils/cookie_options.js create mode 100644 backend/utils/cookie_options.ts delete mode 100644 backend/utils/middleware.js create mode 100644 backend/utils/middleware.ts diff --git a/.gitignore b/.gitignore index aa46cdd2..0c8e9c93 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ backend/node_modules /node_modules coverage/ .idea/ +backend/dist package-lock.json \ No newline at end of file diff --git a/backend/api/index.js b/backend/api/index.js deleted file mode 100644 index 35b79442..00000000 --- a/backend/api/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import app from '../app.js'; -export default app; diff --git a/backend/api/index.ts b/backend/api/index.ts new file mode 100644 index 00000000..d5d94437 --- /dev/null +++ b/backend/api/index.ts @@ -0,0 +1,2 @@ +import app from '../app'; +export default app; diff --git a/backend/app.js b/backend/app.ts similarity index 73% rename from backend/app.js rename to backend/app.ts index 38e4e7a6..bfd59416 100644 --- a/backend/app.js +++ b/backend/app.ts @@ -2,20 +2,20 @@ import compression from 'compression'; import cookieParser from 'cookie-parser'; import cors from 'cors'; import express from 'express'; -import { FRONTEND_URL } from './config/utils.js'; -import authRouter from './routes/auth.js'; -import postsRouter from './routes/posts.js'; -import userRouter from './routes/user.js'; -import errorMiddleware from './middlewares/error-middleware.js'; -import passport from './config/passport.js'; +import authRouter from './routes/auth'; +import postsRouter from './routes/posts'; +import userRouter from './routes/user'; +import errorMiddleware from './middlewares/error-middleware'; +import passport from './config/passport'; import session from 'express-session'; +import { FRONTEND_URL } from './config/utils'; const app = express(); app.use( cors({ // added origin - origin: [FRONTEND_URL, 'http://localhost:3000'], + origin: ['http://localhost:3000'], credentials: true, }) ); diff --git a/backend/config/db.js b/backend/config/db.ts similarity index 68% rename from backend/config/db.js rename to backend/config/db.ts index 51117b35..ea0545c8 100644 --- a/backend/config/db.js +++ b/backend/config/db.ts @@ -1,13 +1,13 @@ -import mongoose from 'mongoose'; -import { MONGODB_URI } from './utils.js'; +import mongoose, { AnyArray } from 'mongoose'; +import { MONGODB_URI } from './utils'; export default async function connectDB() { try { - await mongoose.connect(MONGODB_URI, { + await mongoose.connect(MONGODB_URI as string, { dbName: 'wanderlust', }); console.log(`Database connected: ${MONGODB_URI}`); - } catch (err) { + } catch (err: any) { console.error(err.message); process.exit(1); } diff --git a/backend/config/passport.js b/backend/config/passport.ts similarity index 80% rename from backend/config/passport.js rename to backend/config/passport.ts index 728b377e..9dd6bcc9 100644 --- a/backend/config/passport.js +++ b/backend/config/passport.ts @@ -1,15 +1,14 @@ import passport from 'passport'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -import User from '../models/user.js'; - +import User from '../models/user'; passport.use( new GoogleStrategy( { - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, + clientID: process.env.GOOGLE_CLIENT_ID as string, + clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, callbackURL: `${process.env.BACKEND_URL}/api/auth/google/callback`, }, - async (accessToken, refreshToken, profile, done) => { + async (accessToken: string, refreshToken: string, profile: any, done: any) => { try { let user = await User.findOne({ googleId: profile.id }); @@ -40,7 +39,7 @@ passport.use( ) ); -passport.serializeUser((user, done) => done(null, user.id)); +passport.serializeUser((user: any, done) => done(null, user.id)); passport.deserializeUser(async (id, done) => { try { diff --git a/backend/config/utils.js b/backend/config/utils.ts similarity index 100% rename from backend/config/utils.js rename to backend/config/utils.ts diff --git a/backend/controllers/auth-controller.js b/backend/controllers/auth-controller.ts similarity index 69% rename from backend/controllers/auth-controller.js rename to backend/controllers/auth-controller.ts index e5dd9300..2e7e375f 100644 --- a/backend/controllers/auth-controller.js +++ b/backend/controllers/auth-controller.ts @@ -1,18 +1,23 @@ -import User from '../models/user.js'; +import User from '../models/user'; import jwt from 'jsonwebtoken'; -import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js'; -import { cookieOptions } from '../utils/cookie_options.js'; -import { JWT_SECRET } from '../config/utils.js'; -import { ApiError } from '../utils/api-error.js'; -import { ApiResponse } from '../utils/api-response.js'; -import { asyncHandler } from '../utils/async-handler.js'; +import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants'; +import { cookieOptions } from '../utils/cookie_options'; +import { JWT_SECRET } from '../config/utils'; +import { ApiError } from '../utils/api-error'; +import { ApiResponse } from '../utils/api-response'; +import { asyncHandler } from '../utils/async-handler'; +import { NextFunction, Request, Response } from 'express'; //REGULAR EMAIL PASSWORD STRATEGY //1.Sign Up -export const signUpWithEmail = asyncHandler(async (req, res) => { + +export const signUpWithEmail = asyncHandler(async (req: Request, res: Response) => { const { userName, fullName, email, password } = req.body; if (!userName || !fullName || !email || !password) { - throw new ApiError(HTTP_STATUS.BAD_REQUEST, RESPONSE_MESSAGES.COMMON.REQUIRED_FIELDS); + throw new ApiError({ + status: HTTP_STATUS.BAD_REQUEST, + message: RESPONSE_MESSAGES.COMMON.REQUIRED_FIELDS, + }); } const existingUser = await User.findOne({ @@ -21,10 +26,16 @@ export const signUpWithEmail = asyncHandler(async (req, res) => { if (existingUser) { if (existingUser.userName === userName) { - throw new ApiError(HTTP_STATUS.BAD_REQUEST, RESPONSE_MESSAGES.USERS.USER_USERNAME_EXISTS); + throw new ApiError({ + status: HTTP_STATUS.BAD_REQUEST, + message: RESPONSE_MESSAGES.USERS.USER_USERNAME_EXISTS, + }); } if (existingUser.email === email) { - throw new ApiError(HTTP_STATUS.BAD_REQUEST, RESPONSE_MESSAGES.USERS.USER_EMAIL_EXISTS); + throw new ApiError({ + status: HTTP_STATUS.BAD_REQUEST, + message: RESPONSE_MESSAGES.USERS.USER_EMAIL_EXISTS, + }); } } @@ -37,12 +48,15 @@ export const signUpWithEmail = asyncHandler(async (req, res) => { try { await user.validate(); - } catch (error) { - const validationErrors = []; + } catch (error: any) { + const validationErrors: any = []; for (const key in error.errors) { - validationErrors.push(error.errors[key].message); + validationErrors.push(error.errors[key].message as never); } - throw new ApiError(HTTP_STATUS.BAD_REQUEST, validationErrors.join(', ')); + throw new ApiError({ + status: HTTP_STATUS.BAD_REQUEST, + errors: validationErrors.join(', '), + }); } const accessToken = await user.generateAccessToken(); @@ -71,10 +85,13 @@ export const signUpWithEmail = asyncHandler(async (req, res) => { }); //2.Sign In -export const signInWithEmailOrUsername = asyncHandler(async (req, res) => { +export const signInWithEmailOrUsername = asyncHandler(async (req: Request, res: Response) => { const { userNameOrEmail, password } = req.body; if (!userNameOrEmail || !password) { - throw new ApiError(HTTP_STATUS.BAD_REQUEST, RESPONSE_MESSAGES.COMMON.REQUIRED_FIELDS); + throw new ApiError({ + status: HTTP_STATUS.BAD_REQUEST, + message: RESPONSE_MESSAGES.COMMON.REQUIRED_FIELDS, + }); } const user = await User.findOne({ @@ -82,13 +99,19 @@ export const signInWithEmailOrUsername = asyncHandler(async (req, res) => { }).select('+password'); if (!user) { - throw new ApiError(HTTP_STATUS.BAD_REQUEST, RESPONSE_MESSAGES.USERS.USER_NOT_EXISTS); + throw new ApiError({ + status: HTTP_STATUS.BAD_REQUEST, + message: RESPONSE_MESSAGES.USERS.USER_NOT_EXISTS, + }); } const isCorrectPassword = await user.isPasswordCorrect(password); if (!isCorrectPassword) { - throw new ApiError(HTTP_STATUS.UNAUTHORIZED, RESPONSE_MESSAGES.USERS.INVALID_PASSWORD); + throw new ApiError({ + status: HTTP_STATUS.UNAUTHORIZED, + message: RESPONSE_MESSAGES.USERS.INVALID_PASSWORD, + }); } const accessToken = await user.generateAccessToken(); const refreshToken = await user.generateRefreshToken(); @@ -115,7 +138,7 @@ export const signInWithEmailOrUsername = asyncHandler(async (req, res) => { }); //Sign Out -export const signOutUser = asyncHandler(async (req, res, next) => { +export const signOutUser = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { await User.findByIdAndUpdate( req.user?._id, { @@ -129,9 +152,14 @@ export const signOutUser = asyncHandler(async (req, res, next) => { ); // Passport.js logout - req.logout((err) => { + req.logout((err: any) => { if (err) { - return next(new ApiError(HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Logout failed')); + return next( + new ApiError({ + status: HTTP_STATUS.INTERNAL_SERVER_ERROR, + message: 'Logout failed', + }) + ); } res @@ -143,7 +171,7 @@ export const signOutUser = asyncHandler(async (req, res, next) => { }); }); // check user -export const isLoggedIn = asyncHandler(async (req, res) => { +export const isLoggedIn = asyncHandler(async (req: Request, res: Response) => { let access_token = req.cookies?.access_token; let refresh_token = req.cookies?.refresh_token; const { _id } = req.params; @@ -155,18 +183,22 @@ export const isLoggedIn = asyncHandler(async (req, res) => { } if (access_token) { try { - await jwt.verify(access_token, JWT_SECRET); + if (JWT_SECRET) { + await jwt.verify(access_token, JWT_SECRET); + } return res .status(HTTP_STATUS.OK) .json(new ApiResponse(HTTP_STATUS.OK, access_token, RESPONSE_MESSAGES.USERS.VALID_TOKEN)); - } catch (error) { + } catch (error: any) { console.log('Access token verification error:', error.message); } } // If access token is not valid, check the refresh token if (refresh_token) { try { - await jwt.verify(refresh_token, JWT_SECRET); + if (JWT_SECRET) { + await jwt.verify(refresh_token, JWT_SECRET); + } const user = await User.findById(_id); if (!user) { return res @@ -180,7 +212,7 @@ export const isLoggedIn = asyncHandler(async (req, res) => { .status(HTTP_STATUS.OK) .cookie('access_token', access_token, cookieOptions) .json(new ApiResponse(HTTP_STATUS.OK, access_token, RESPONSE_MESSAGES.USERS.VALID_TOKEN)); - } catch (error) { + } catch (error: any) { console.log('Refresh token verification error:', error.message); } } @@ -202,7 +234,9 @@ export const isLoggedIn = asyncHandler(async (req, res) => { } try { - await jwt.verify(refreshToken, JWT_SECRET); + if (JWT_SECRET) { + await jwt.verify(refreshToken, JWT_SECRET); + } access_token = await user.generateAccessToken(); refresh_token = await user.generateRefreshToken(); @@ -213,7 +247,7 @@ export const isLoggedIn = asyncHandler(async (req, res) => { .cookie('access_token', access_token, cookieOptions) .cookie('refresh_token', refresh_token, cookieOptions) .json(new ApiResponse(HTTP_STATUS.OK, access_token, RESPONSE_MESSAGES.USERS.VALID_TOKEN)); - } catch (error) { + } catch (error: any) { return res .status(HTTP_STATUS.UNAUTHORIZED) .json( diff --git a/backend/controllers/posts-controller.js b/backend/controllers/posts-controller.ts similarity index 82% rename from backend/controllers/posts-controller.js rename to backend/controllers/posts-controller.ts index e275e7fa..de88debb 100644 --- a/backend/controllers/posts-controller.js +++ b/backend/controllers/posts-controller.ts @@ -1,8 +1,9 @@ -import Post from '../models/post.js'; -import User from '../models/user.js'; -import { deleteDataFromCache, storeDataInCache } from '../utils/cache-posts.js'; -import { HTTP_STATUS, REDIS_KEYS, RESPONSE_MESSAGES, validCategories } from '../utils/constants.js'; -export const createPostHandler = async (req, res) => { +import Post from '../models/post'; +import User from '../models/user'; +import { deleteDataFromCache, storeDataInCache } from '../utils/cache-posts'; +import { HTTP_STATUS, REDIS_KEYS, RESPONSE_MESSAGES, validCategories } from '../utils/constants'; +import { Request, Response, NextFunction } from 'express'; +export const createPostHandler = async (req: Request, res: Response) => { try { const { title, @@ -58,32 +59,32 @@ export const createPostHandler = async (req, res) => { await User.findByIdAndUpdate(userId, { $push: { posts: savedPost._id } }); res.status(HTTP_STATUS.OK).json(savedPost); - } catch (err) { + } catch (err: any) { res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); } }; -export const getAllPostsHandler = async (req, res) => { +export const getAllPostsHandler = async (req: Request, res: Response) => { try { const posts = await Post.find(); await storeDataInCache(REDIS_KEYS.ALL_POSTS, posts); return res.status(HTTP_STATUS.OK).json(posts); - } catch (err) { + } catch (err: any) { res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); } }; -export const getFeaturedPostsHandler = async (req, res) => { +export const getFeaturedPostsHandler = async (req: Request, res: Response) => { try { const featuredPosts = await Post.find({ isFeaturedPost: true }); await storeDataInCache(REDIS_KEYS.FEATURED_POSTS, featuredPosts); res.status(HTTP_STATUS.OK).json(featuredPosts); - } catch (err) { + } catch (err: any) { res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); } }; -export const getPostByCategoryHandler = async (req, res) => { +export const getPostByCategoryHandler = async (req: Request, res: Response) => { const category = req.params.category; try { // Validation - check if category is valid @@ -95,22 +96,22 @@ export const getPostByCategoryHandler = async (req, res) => { const categoryPosts = await Post.find({ categories: category }); res.status(HTTP_STATUS.OK).json(categoryPosts); - } catch (err) { + } catch (err: any) { res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); } }; -export const getLatestPostsHandler = async (req, res) => { +export const getLatestPostsHandler = async (req: Request, res: Response) => { try { const latestPosts = await Post.find().sort({ timeOfPost: -1 }); await storeDataInCache(REDIS_KEYS.LATEST_POSTS, latestPosts); res.status(HTTP_STATUS.OK).json(latestPosts); - } catch (err) { + } catch (err: any) { res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); } }; -export const getPostByIdHandler = async (req, res) => { +export const getPostByIdHandler = async (req: Request, res: Response) => { try { const post = await Post.findById(req.params.id); @@ -120,12 +121,12 @@ export const getPostByIdHandler = async (req, res) => { } res.status(HTTP_STATUS.OK).json(post); - } catch (err) { + } catch (err: any) { res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); } }; -export const updatePostHandler = async (req, res) => { +export const updatePostHandler = async (req: Request, res: Response) => { try { const updatedPost = await Post.findByIdAndUpdate(req.params.id, req.body, { new: true, @@ -140,12 +141,12 @@ export const updatePostHandler = async (req, res) => { await deleteDataFromCache(REDIS_KEYS.FEATURED_POSTS), await deleteDataFromCache(REDIS_KEYS.LATEST_POSTS), await res.status(HTTP_STATUS.OK).json(updatedPost); - } catch (err) { + } catch (err: any) { res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); } }; -export const deletePostByIdHandler = async (req, res) => { +export const deletePostByIdHandler = async (req: Request, res: Response) => { try { const post = await Post.findByIdAndDelete(req.params.id); @@ -160,24 +161,24 @@ export const deletePostByIdHandler = async (req, res) => { await deleteDataFromCache(REDIS_KEYS.FEATURED_POSTS), await deleteDataFromCache(REDIS_KEYS.LATEST_POSTS), res.status(HTTP_STATUS.OK).json({ message: RESPONSE_MESSAGES.POSTS.DELETED }); - } catch (err) { + } catch (err: any) { res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); } }; -export const getRelatedPostsByCategories = async (req, res) => { +export const getRelatedPostsByCategories = async (req: Request, res: Response) => { const { categories } = req.query; if (!categories) { return res .status(HTTP_STATUS.NOT_FOUND) - .json({ message: RESPONSE_MESSAGES.POSTS.CATEGORIES_NOTFOUND }); + .json({ message: RESPONSE_MESSAGES.POSTS.INVALID_CATEGORY }); } try { const filteredCategoryPosts = await Post.find({ categories: { $in: categories }, }); res.status(HTTP_STATUS.OK).json(filteredCategoryPosts); - } catch (err) { + } catch (err: any) { res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); } }; diff --git a/backend/controllers/user-controller.js b/backend/controllers/user-controller.ts similarity index 82% rename from backend/controllers/user-controller.js rename to backend/controllers/user-controller.ts index 489158b6..dfda9028 100644 --- a/backend/controllers/user-controller.js +++ b/backend/controllers/user-controller.ts @@ -1,8 +1,9 @@ -import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js'; -import User from '../models/user.js'; -import { Role } from '../types/role-type.js'; +import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants'; +import User from '../models/user'; +import { Role } from '../types/role-type'; +import { Request, Response } from 'express'; -export const getAllUserHandler = async (req, res) => { +export const getAllUserHandler = async (req: Request, res: Response) => { try { const users = await User.find().select('_id fullName role email'); return res.status(HTTP_STATUS.OK).json({ users }); @@ -14,7 +15,7 @@ export const getAllUserHandler = async (req, res) => { } }; -export const changeUserRoleHandler = async (req, res) => { +export const changeUserRoleHandler = async (req: Request, res: Response) => { try { const userId = req.params.userId; const { role } = req.body; @@ -40,7 +41,7 @@ export const changeUserRoleHandler = async (req, res) => { } }; -export const deleteUserHandler = async (req, res) => { +export const deleteUserHandler = async (req: Request, res: Response) => { try { const userId = req.params.userId; const user = await User.findByIdAndDelete(userId); diff --git a/backend/eslint.config.js b/backend/eslint.config.mjs similarity index 98% rename from backend/eslint.config.js rename to backend/eslint.config.mjs index 98e6a9bb..117f6901 100644 --- a/backend/eslint.config.js +++ b/backend/eslint.config.mjs @@ -4,4 +4,4 @@ import pluginJs from '@eslint/js'; export default [ { languageOptions: { globals: { ...globals.node } } }, pluginJs.configs.recommended, -]; +]; \ No newline at end of file diff --git a/backend/middlewares/auth-middleware.js b/backend/middlewares/auth-middleware.js deleted file mode 100644 index 363645f3..00000000 --- a/backend/middlewares/auth-middleware.js +++ /dev/null @@ -1,30 +0,0 @@ -import { JWT_SECRET } from '../config/utils.js'; -import { ApiError } from '../utils/api-error.js'; -import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js'; -import jwt from 'jsonwebtoken'; -import { Role } from '../types/role-type.js'; -import User from '../models/user.js'; - -export const authMiddleware = async (req, res, next) => { - const token = req.cookies?.access_token; - if (!token) { - return next(new ApiError(HTTP_STATUS.BAD_REQUEST, RESPONSE_MESSAGES.USERS.RE_LOGIN)); - } - - try { - const payload = jwt.verify(token, JWT_SECRET); - req.user = await User.findById(payload.id); - next(); - } catch (error) { - console.log('Token verification error:', error.message); - return next(new ApiError(HTTP_STATUS.FORBIDDEN, RESPONSE_MESSAGES.USERS.INVALID_TOKEN)); - } -}; - -export const isAdminMiddleware = async (req, res, next) => { - const role = req.user.role; - if (role !== Role.Admin) { - return new ApiError(HTTP_STATUS.UNAUTHORIZED, RESPONSE_MESSAGES.USERS.UNAUTHORIZED_USER); - } - next(); -}; diff --git a/backend/middlewares/auth-middleware.ts b/backend/middlewares/auth-middleware.ts new file mode 100644 index 00000000..a55c2524 --- /dev/null +++ b/backend/middlewares/auth-middleware.ts @@ -0,0 +1,50 @@ +import { JWT_SECRET } from '../config/utils'; +import { ApiError } from '../utils/api-error'; +import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants'; +import jwt from 'jsonwebtoken'; +import { Role } from '../types/role-type'; +import User from '../models/user'; + +import { Request, Response, NextFunction } from 'express'; + +interface JwtPayload { + _id: string; +} + +export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => { + const token = req.cookies?.access_token; + if (!token) { + return next( + new ApiError({ + status: HTTP_STATUS.BAD_REQUEST, + message: RESPONSE_MESSAGES.USERS.RE_LOGIN, + }) + ); + } + + try { + const { _id } = jwt.verify(token, JWT_SECRET as string) as JwtPayload; + + req.user = await User.findById(_id); + next(); + } catch (error: any) { + console.log('Token verification error:', error.message); + return next( + new ApiError({ + status: HTTP_STATUS.FORBIDDEN, + message: RESPONSE_MESSAGES.USERS.INVALID_TOKEN, + }) + ); + } +}; + +export const isAdminMiddleware = async (req: Request, res: Response, next: NextFunction) => { + const role = req.user.role; + if (role !== Role.Admin) { + return new ApiError({ + status: HTTP_STATUS.UNAUTHORIZED, + message: RESPONSE_MESSAGES.USERS.UNAUTHORIZED_USER, + }); + } + next(); +}; diff --git a/backend/middlewares/error-middleware.js b/backend/middlewares/error-middleware.ts similarity index 71% rename from backend/middlewares/error-middleware.js rename to backend/middlewares/error-middleware.ts index e4d0b6d6..dcacb2c8 100644 --- a/backend/middlewares/error-middleware.js +++ b/backend/middlewares/error-middleware.ts @@ -1,6 +1,7 @@ -import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js'; +import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants'; +import { Request, Response, NextFunction } from 'express'; -const errorMiddleware = (err, req, res, next) => { +const errorMiddleware = (err: any, req: Request, res: Response, next: NextFunction) => { console.error(err.stack); res.status(err.status || HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ status: err.status || HTTP_STATUS.INTERNAL_SERVER_ERROR, diff --git a/backend/middlewares/post-middleware.js b/backend/middlewares/post-middleware.ts similarity index 74% rename from backend/middlewares/post-middleware.js rename to backend/middlewares/post-middleware.ts index 4a7dd72d..b214343f 100644 --- a/backend/middlewares/post-middleware.js +++ b/backend/middlewares/post-middleware.ts @@ -1,7 +1,8 @@ -import Post from '../models/post.js'; -import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js'; +import Post from '../models/post'; +import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants'; +import { Request, Response, NextFunction } from 'express'; -export const isAuthorMiddleware = async (req, res, next) => { +export const isAuthorMiddleware = async (req: Request, res: Response, next: NextFunction) => { try { const userId = req.user._id; const postId = req.params.id; @@ -17,7 +18,7 @@ export const isAuthorMiddleware = async (req, res, next) => { .json({ message: RESPONSE_MESSAGES.POSTS.NOT_ALLOWED }); } next(); - } catch (error) { + } catch (error: any) { res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: error.message }); } }; diff --git a/backend/models/post.js b/backend/models/post.ts similarity index 100% rename from backend/models/post.js rename to backend/models/post.ts diff --git a/backend/models/user.js b/backend/models/user.ts similarity index 65% rename from backend/models/user.js rename to backend/models/user.ts index c564fba4..ec0ea110 100644 --- a/backend/models/user.js +++ b/backend/models/user.ts @@ -1,10 +1,25 @@ -import { Schema, model } from 'mongoose'; +import { Schema, model, Document } from 'mongoose'; import JWT from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; import crypto from 'crypto'; -import { ACCESS_TOKEN_EXPIRES_IN, JWT_SECRET, REFRESH_TOKEN_EXPIRES_IN } from '../config/utils.js'; -import { Role } from '../types/role-type.js'; +import { ACCESS_TOKEN_EXPIRES_IN, JWT_SECRET, REFRESH_TOKEN_EXPIRES_IN } from '../config/utils'; +import { Role } from '../types/role-type'; +interface UserObject extends Document { + id: number; + userName: string; + fullName: string; + email: string; + password?: string; + avatar: string; + role: string; + posts: Schema.Types.ObjectId; + refreshToken?: string; + isPasswordCorrect(password: string): Promise; + generateAccessToken(): Promise; + generateRefreshToken(): Promise; + generateResetToken(): Promise; +} const userSchema = new Schema( { userName: { @@ -74,40 +89,44 @@ userSchema.pre('save', async function (next) { if (!this.isModified('password')) { return next(); } - this.password = await bcrypt.hash(this.password, 10); + this.password! = await bcrypt.hash(this.password!, 10); }); userSchema.methods = { - isPasswordCorrect: async function (password) { + isPasswordCorrect: async function (password: string) { return await bcrypt.compare(password, this.password); }, generateAccessToken: async function () { - return JWT.sign( - { - _id: this._id, - username: this.userName, - email: this.email, - role: this.role, - }, - JWT_SECRET, - { - expiresIn: ACCESS_TOKEN_EXPIRES_IN, - } - ); + if (JWT_SECRET) { + return JWT.sign( + { + _id: this._id, + username: this.userName, + email: this.email, + role: this.role, + }, + JWT_SECRET, + { + expiresIn: ACCESS_TOKEN_EXPIRES_IN, + } + ); + } }, generateRefreshToken: async function () { - return JWT.sign( - { - _id: this._id, - username: this.userName, - email: this.email, - role: this.role, - }, - JWT_SECRET, - { - expiresIn: REFRESH_TOKEN_EXPIRES_IN, - } - ); + if (JWT_SECRET) { + return JWT.sign( + { + _id: this._id, + username: this.userName, + email: this.email, + role: this.role, + }, + JWT_SECRET, + { + expiresIn: REFRESH_TOKEN_EXPIRES_IN, + } + ); + } }, generateResetToken: async function () { const resetToken = crypto.randomBytes(20).toString('hex'); @@ -117,6 +136,6 @@ userSchema.methods = { }, }; -const User = model('User', userSchema); +const User = model('User', userSchema); export default User; diff --git a/backend/package.json b/backend/package.json index 9d666138..58f5e0d9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,17 @@ { - "type": "module", + "type": "commonjs", "dependencies": { + "@types/bcrypt": "^5.0.2", + "@types/bcryptjs": "^2.4.6", + "@types/compression": "^1.7.5", + "@types/cookie-parser": "^1.4.7", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", + "@types/jsonwebtoken": "^9.0.6", + "@types/passport": "^1.0.16", + "@types/passport-google-oauth20": "^2.0.16", + "@types/supertest": "^6.0.2", "axios": "^1.6.8", "bcryptjs": "^2.4.3", "compression": "^1.7.4", @@ -38,9 +49,13 @@ }, "devDependencies": { "@babel/preset-env": "^7.23.2", + "@eslint/js": "^9.9.0", "@stylistic/eslint-plugin-js": "^2.1.0", + "@types/eslint": "^9.6.0", + "@typescript-eslint/eslint-plugin": "^8.1.0", + "@typescript-eslint/parser": "^8.1.0", "babel-jest": "^29.7.0", - "eslint": "^9.3.0", + "eslint": "^9.9.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-babel": "^5.3.1", "eslint-plugin-prettier": "^5.1.3", @@ -48,6 +63,7 @@ "jest": "^29.7.0", "lint-staged": "^15.2.2", "prettier": "^3.2.5", - "supertest": "^6.3.3" + "supertest": "^6.3.3", + "typescript": "^5.5.4" } } diff --git a/backend/routes/auth.js b/backend/routes/auth.ts similarity index 74% rename from backend/routes/auth.js rename to backend/routes/auth.ts index 85b498b3..a63e34c6 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.ts @@ -1,13 +1,14 @@ import { Router } from 'express'; -import { authMiddleware } from '../middlewares/auth-middleware.js'; -import passport from '../config/passport.js'; +import { authMiddleware } from '../middlewares/auth-middleware'; +import passport from '../config/passport'; import jwt from 'jsonwebtoken'; +import { Request, Response } from 'express'; import { signUpWithEmail, signInWithEmailOrUsername, signOutUser, isLoggedIn, -} from '../controllers/auth-controller.js'; +} from '../controllers/auth-controller'; const router = Router(); @@ -21,8 +22,11 @@ router.get('/google', passport.authenticate('google', { scope: ['profile', 'emai router.get( '/google/callback', passport.authenticate('google', { failureRedirect: '/' }), - (req, res) => { - const token = jwt.sign({ id: req.user._id }, process.env.JWT_SECRET, { expiresIn: '1h' }); + (req: Request, res: Response) => { + let token = ''; + if (process.env.JWT_SECRET) { + token = jwt.sign({ id: req.user._id }, process.env.JWT_SECRET, { expiresIn: '1h' }); + } res.cookie('access_token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', diff --git a/backend/routes/posts.js b/backend/routes/posts.ts similarity index 92% rename from backend/routes/posts.js rename to backend/routes/posts.ts index bd393ccd..50f908d9 100644 --- a/backend/routes/posts.js +++ b/backend/routes/posts.ts @@ -10,9 +10,9 @@ import { getRelatedPostsByCategories, updatePostHandler, } from '../controllers/posts-controller.js'; -import { REDIS_KEYS } from '../utils/constants.js'; -import { cacheHandler } from '../utils/middleware.js'; -import { isAdminMiddleware, authMiddleware } from '../middlewares/auth-middleware.js'; +import { REDIS_KEYS } from '../utils/constants'; +import { cacheHandler } from '../utils/middleware'; +import { isAdminMiddleware, authMiddleware } from '../middlewares/auth-middleware'; import { isAuthorMiddleware } from '../middlewares/post-middleware.js'; const router = Router(); diff --git a/backend/routes/user.js b/backend/routes/user.ts similarity index 88% rename from backend/routes/user.js rename to backend/routes/user.ts index 73ba4a7a..1caa2e63 100644 --- a/backend/routes/user.js +++ b/backend/routes/user.ts @@ -1,10 +1,10 @@ import { Router } from 'express'; -import { isAdminMiddleware, authMiddleware } from '../middlewares/auth-middleware.js'; +import { isAdminMiddleware, authMiddleware } from '../middlewares/auth-middleware'; import { changeUserRoleHandler, deleteUserHandler, getAllUserHandler, -} from '../controllers/user-controller.js'; +} from '../controllers/user-controller'; const router = Router(); diff --git a/backend/server.js b/backend/server.js deleted file mode 100644 index 0e6f4e75..00000000 --- a/backend/server.js +++ /dev/null @@ -1,20 +0,0 @@ -import app from './app.js'; -import connectDB from './config/db.js'; -import { PORT } from './config/utils.js'; -import { connectToRedis } from './services/redis.js'; - -const port = PORT || 8080; - -// Connect to redis -connectToRedis(); - -//Connect to Mongodb -connectDB() - .then(() => { - app.listen(port, () => { - console.log(`Server is running on port ${port}`); - }); - }) - .catch((error) => { - console.log('MongoDB connection failed:', error); - }); diff --git a/backend/server.ts b/backend/server.ts new file mode 100644 index 00000000..4afe582f --- /dev/null +++ b/backend/server.ts @@ -0,0 +1,24 @@ +import app from './app'; +import connectDB from './config/db'; +import { PORT } from './config/utils'; +import { connectToRedis } from './services/redis.js'; + +const server = () => { + const port = PORT || 8080; + + // Redis connection + connectToRedis(); + // mongodb connection + connectDB() + .then(() => { + app.listen(port, () => { + console.log(`Server is running on port ${port}`); + }); + }) + .catch((error) => { + console.log('MongoDB connection failed:', error); + }); +}; +server(); + +export default server; diff --git a/backend/services/redis.js b/backend/services/redis.ts similarity index 83% rename from backend/services/redis.js rename to backend/services/redis.ts index 065b1f9f..e121e009 100644 --- a/backend/services/redis.js +++ b/backend/services/redis.ts @@ -1,7 +1,7 @@ import { createClient } from 'redis'; -import { REDIS_URL } from '../config/utils.js'; +import { REDIS_URL } from '../config/utils'; -let redis = null; +let redis: any = null; export async function connectToRedis() { try { @@ -14,7 +14,7 @@ export async function connectToRedis() { } else { console.log('Redis not configured, cache disabled.'); } - } catch (error) { + } catch (error: any) { console.error('Error connecting to Redis:', error.message); } } diff --git a/backend/tests/integration/controllers/posts-controller.test.js b/backend/tests/integration/controllers/posts-controller.test.ts similarity index 95% rename from backend/tests/integration/controllers/posts-controller.test.js rename to backend/tests/integration/controllers/posts-controller.test.ts index cb747db0..8e44d24e 100644 --- a/backend/tests/integration/controllers/posts-controller.test.js +++ b/backend/tests/integration/controllers/posts-controller.test.ts @@ -1,16 +1,16 @@ import mongoose from 'mongoose'; import request from 'supertest'; -import Post from '../../../models/post.js'; -import server from '../../../server.js'; -import { validCategories, HTTP_STATUS, RESPONSE_MESSAGES } from '../../../utils/constants.js'; -import { createPostObject } from '../../utils/helper-objects.js'; +import Post from '../../../models/post'; +import server from '../../../server'; +import { validCategories, HTTP_STATUS, RESPONSE_MESSAGES } from '../../../utils/constants'; +import { createPostObject } from '../../utils/helper-objects'; import { expect, jest, it, afterAll, describe } from '@jest/globals'; afterAll(async () => { await mongoose.disconnect(); }); -let postId; +let postId: any; const invalidPostId = '609c16c69405b14574c99999'; describe('Integration Tests: Post creation', () => { it('Post creation: Success - All fields are valid', async () => { @@ -21,7 +21,7 @@ describe('Integration Tests: Post creation', () => { expect(response.status).toBe(HTTP_STATUS.OK); expect(response.body).toHaveProperty('_id'); expect(fetchedPost).not.toBeNull(); - expect(fetchedPost.title).toBe(createPostObject().title); + expect(fetchedPost?.title).toBe(createPostObject().title); }); it('Post creation: Failure - Missing required fields', async () => { @@ -128,7 +128,7 @@ describe('Integration Tests: Update Post', () => { expect(response.status).toBe(HTTP_STATUS.OK); expect(updatedPost).not.toBeNull(); - expect(updatedPost.title).toBe('Updated Post'); + expect(updatedPost?.title).toBe('Updated Post'); }); it('Update Post: Failure - Invalid post ID', async () => { diff --git a/backend/tests/teardown.js b/backend/tests/teardown.ts similarity index 100% rename from backend/tests/teardown.js rename to backend/tests/teardown.ts diff --git a/backend/tests/unit/controllers/posts-controller.test.js b/backend/tests/unit/controllers/posts-controller.test.ts similarity index 77% rename from backend/tests/unit/controllers/posts-controller.test.js rename to backend/tests/unit/controllers/posts-controller.test.ts index a315ee36..9b0a40b4 100644 --- a/backend/tests/unit/controllers/posts-controller.test.js +++ b/backend/tests/unit/controllers/posts-controller.test.ts @@ -7,11 +7,11 @@ import { getPostByCategoryHandler, getPostByIdHandler, updatePostHandler, -} from '../../../controllers/posts-controller.js'; -import Post from '../../../models/post.js'; +} from '../../../controllers/posts-controller'; +import Post from '../../../models/post'; import { expect, jest, it, describe } from '@jest/globals'; -import { validCategories, HTTP_STATUS, RESPONSE_MESSAGES } from '../../../utils/constants.js'; -import { createPostObject, createRequestObject, res } from '../../utils/helper-objects.js'; +import { validCategories, HTTP_STATUS, RESPONSE_MESSAGES } from '../../../utils/constants'; +import { createPostObject, createRequestObject, res } from '../../utils/helper-objects'; jest.mock('../../../models/post.js', () => ({ __esModule: true, @@ -21,11 +21,9 @@ jest.mock('../../../models/post.js', () => ({ describe('createPostHandler', () => { it('Post creation: Success - All fields are valid', async () => { const postObject = createPostObject(); - const req = createRequestObject({ body: postObject }); + const req: any = createRequestObject({ body: postObject }); - Post.mockImplementationOnce(() => ({ - save: jest.fn().mockResolvedValueOnce(postObject), - })); + jest.spyOn(Post.prototype, 'save').mockImplementationOnce(() => Promise.resolve(postObject)); await createPostHandler(req, res); @@ -39,7 +37,7 @@ describe('createPostHandler', () => { const postObject = createPostObject({ imageLink: 'https://www.forTestingPurposeOnly/my-image.gif', // Invalid image URL }); - const req = createRequestObject({ body: postObject }); + const req: any = createRequestObject({ body: postObject }); await createPostHandler(req, res); @@ -54,7 +52,7 @@ describe('createPostHandler', () => { delete postObject.title; delete postObject.authorName; - const req = createRequestObject({ body: postObject }); + const req: any = createRequestObject({ body: postObject }); await createPostHandler(req, res); @@ -66,7 +64,7 @@ describe('createPostHandler', () => { const postObject = createPostObject({ categories: [validCategories[0], validCategories[1], validCategories[2], validCategories[3]], // 4 categories }); - const req = createRequestObject({ body: postObject }); + const req: any = createRequestObject({ body: postObject }); await createPostHandler(req, res); @@ -78,13 +76,9 @@ describe('createPostHandler', () => { it('Post creation: Failure - Internal server error', async () => { const postObject = createPostObject(); - const req = createRequestObject({ body: postObject }); + const req: any = createRequestObject({ body: postObject }); - Post.mockImplementationOnce(() => ({ - save: jest - .fn() - .mockRejectedValueOnce(new Error(RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR)), - })); + jest.spyOn(Post.prototype, 'save').mockImplementationOnce(() => Promise.resolve(postObject)); await createPostHandler(req, res); @@ -97,7 +91,7 @@ describe('createPostHandler', () => { describe('getAllPostsHandler', () => { it('Get all posts: Success - Retrieving all posts list', async () => { - const req = createRequestObject(); + const req: any = createRequestObject(); const mockPosts = [ createPostObject({ title: 'Test Post - 1' }), @@ -105,7 +99,7 @@ describe('getAllPostsHandler', () => { createPostObject({ title: 'Test Post - 3' }), ]; - Post.find = jest.fn().mockResolvedValueOnce(mockPosts); + jest.spyOn(Post, 'find').mockResolvedValue(mockPosts); await getAllPostsHandler(req, res); @@ -114,12 +108,10 @@ describe('getAllPostsHandler', () => { }); it('Get all posts: Failure - Internal Server Error', async () => { - const req = createRequestObject(); - - Post.find = jest - .fn() + const req: any = createRequestObject(); + jest + .spyOn(Post, 'find') .mockRejectedValueOnce(new Error(RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR)); - await getAllPostsHandler(req, res); expect(res.status).toHaveBeenCalledWith(HTTP_STATUS.INTERNAL_SERVER_ERROR); @@ -131,7 +123,7 @@ describe('getAllPostsHandler', () => { describe('getFeaturedPostsHandler', () => { it('Get featured posts: Success - Retrieving all featured posts list', async () => { - const req = createRequestObject(); + const req: any = createRequestObject(); const mockFeaturedPosts = [ createPostObject({ title: 'Test Post - 1', isFeaturedPost: true }), @@ -139,7 +131,7 @@ describe('getFeaturedPostsHandler', () => { createPostObject({ title: 'Test Post - 3', isFeaturedPost: true }), ]; - Post.find = jest.fn().mockResolvedValueOnce(mockFeaturedPosts); + jest.spyOn(Post, 'find').mockResolvedValue(mockFeaturedPosts); await getFeaturedPostsHandler(req, res); @@ -148,12 +140,10 @@ describe('getFeaturedPostsHandler', () => { }); it('Get featured posts: Failure - Internal Server Error', async () => { - const req = createRequestObject(); - - Post.find = jest - .fn() + const req: any = createRequestObject(); + jest + .spyOn(Post, 'find') .mockRejectedValueOnce(new Error(RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR)); - await getFeaturedPostsHandler(req, res); expect(res.status).toHaveBeenCalledWith(HTTP_STATUS.INTERNAL_SERVER_ERROR); @@ -165,7 +155,7 @@ describe('getFeaturedPostsHandler', () => { describe('getPostByCategoryHandler', () => { it('Get posts by category: Success - Retrieving posts list of specified category', async () => { - const req = createRequestObject({ params: { category: validCategories[1] } }); + const req: any = createRequestObject({ params: { category: validCategories[1] } }); const mockPosts = [ createPostObject({ title: 'Test Post - 1', categories: [validCategories[1]] }), @@ -173,7 +163,7 @@ describe('getPostByCategoryHandler', () => { createPostObject({ title: 'Test Post - 3', categories: [validCategories[1]] }), ]; - Post.find = jest.fn().mockResolvedValueOnce(mockPosts); + jest.spyOn(Post, 'find').mockResolvedValue(mockPosts); await getPostByCategoryHandler(req, res); @@ -182,7 +172,7 @@ describe('getPostByCategoryHandler', () => { }); it('Get posts by category: Failure - Invalid category', async () => { - const req = createRequestObject({ params: { category: 'Invalid Category' } }); + const req: any = createRequestObject({ params: { category: 'Invalid Category' } }); await getPostByCategoryHandler(req, res); @@ -191,10 +181,10 @@ describe('getPostByCategoryHandler', () => { }); it('Get posts by category: Failure - Internal Server Error', async () => { - const req = createRequestObject({ params: { category: validCategories[1] } }); + const req: any = createRequestObject({ params: { category: validCategories[1] } }); - Post.find = jest - .fn() + jest + .spyOn(Post, 'find') .mockRejectedValueOnce(new Error(RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR)); await getPostByCategoryHandler(req, res); @@ -208,7 +198,7 @@ describe('getPostByCategoryHandler', () => { describe('getLatestPostsHandler', () => { it('Get latest posts: Success - Retrieving most recent posts list', async () => { - const req = createRequestObject(); + const req: any = createRequestObject(); const mockPosts = [ createPostObject({ title: 'Test Post - 1' }), @@ -216,9 +206,7 @@ describe('getLatestPostsHandler', () => { createPostObject({ title: 'Test Post - 3' }), ]; - Post.find.mockReturnValueOnce({ - sort: jest.fn().mockResolvedValueOnce(mockPosts), - }); + jest.spyOn(Post, 'find').mockResolvedValueOnce(mockPosts); await getLatestPostsHandler(req, res); @@ -227,13 +215,11 @@ describe('getLatestPostsHandler', () => { }); it('Get latest posts: Failure - Internal Server Error', async () => { - const req = createRequestObject(); + const req: any = createRequestObject(); - Post.find.mockReturnValueOnce({ - sort: jest - .fn() - .mockRejectedValueOnce(new Error(RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR)), - }); + jest + .spyOn(Post, 'find') + .mockRejectedValueOnce(new Error(RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR)); await getLatestPostsHandler(req, res); @@ -246,11 +232,11 @@ describe('getLatestPostsHandler', () => { describe('getPostByIdHandler', () => { it('Get post by ID: Success - Retrieving Specific Post', async () => { - const req = createRequestObject({ params: { id: '6910293383' } }); + const req: any = createRequestObject({ params: { id: '6910293383' } }); - const mockPost = createPostObject({ _id: '6910293383' }); + const mockPost: any = createPostObject({ _id: '6910293383' }); - Post.findById = jest.fn().mockResolvedValueOnce(mockPost); + jest.spyOn(Post, 'find').mockResolvedValueOnce(mockPost); await getPostByIdHandler(req, res); @@ -259,9 +245,9 @@ describe('getPostByIdHandler', () => { }); it('Get post by ID: Failure - Post not found (Specified post ID is invalid)', async () => { - const req = createRequestObject({ params: { id: '6910293383' } }); + const req: any = createRequestObject({ params: { id: '6910293383' } }); - Post.findById = jest.fn().mockResolvedValueOnce(null); + jest.spyOn(Post, 'findById').mockResolvedValue(null); await getPostByIdHandler(req, res); @@ -270,10 +256,10 @@ describe('getPostByIdHandler', () => { }); it('Get post by ID: Failure - Internal Server Error', async () => { - const req = createRequestObject({ params: { id: '6910293383' } }); + const req: any = createRequestObject({ params: { id: '6910293383' } }); - Post.findById = jest - .fn() + jest + .spyOn(Post, 'findById') .mockRejectedValueOnce(new Error(RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR)); await getPostByIdHandler(req, res); @@ -287,7 +273,7 @@ describe('getPostByIdHandler', () => { describe('updatePostHandler', () => { it('Update post: Success - Modifying post content', async () => { - const req = createRequestObject({ + const req: any = createRequestObject({ params: { id: '6910293383' }, body: { title: 'Updated Post' }, }); @@ -295,7 +281,7 @@ describe('updatePostHandler', () => { const mockPost = createPostObject({ _id: '6910293383', title: 'Updated Post' }); // Mock the behavior of Post.findByIdAndUpdate - Post.findByIdAndUpdate = jest.fn().mockResolvedValueOnce(mockPost); + jest.spyOn(Post, 'findByIdAndUpdate').mockResolvedValueOnce(mockPost); await updatePostHandler(req, res); @@ -304,13 +290,13 @@ describe('updatePostHandler', () => { }); it('Update post: Failure - Post not found (Specified post ID is invalid)', async () => { - const req = createRequestObject({ + const req: any = createRequestObject({ params: { id: '6910293383' }, body: { title: 'Updated Post' }, }); // Mock the behavior of Post.findByIdAndUpdate - Post.findByIdAndUpdate = jest.fn().mockResolvedValueOnce(null); + jest.spyOn(Post, 'findByIdAndUpdate').mockResolvedValueOnce(null); await updatePostHandler(req, res); @@ -319,15 +305,14 @@ describe('updatePostHandler', () => { }); it('Update post: Failure - Internal Server Error', async () => { - const req = createRequestObject({ + const req: any = createRequestObject({ params: { id: '6910293383' }, body: { title: 'Updated Post' }, }); // Mock the behavior of Post.findByIdAndUpdate - Post.findByIdAndUpdate = jest - .fn() + jest + .spyOn(Post, 'findByIdAndUpdate') .mockRejectedValueOnce(new Error(RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR)); - await updatePostHandler(req, res); expect(res.status).toHaveBeenCalledWith(HTTP_STATUS.INTERNAL_SERVER_ERROR); @@ -339,12 +324,12 @@ describe('updatePostHandler', () => { describe('deletePostByIdHandler', () => { it('Delete Post: Success - Removing Post with specified ID', async () => { - const req = createRequestObject({ params: { id: '6910293383' } }); + const req: any = createRequestObject({ params: { id: '6910293383' } }); const mockPost = createPostObject({ _id: '6910293383' }); // Mock the behavior of Post.findByIdAndRemove - Post.findByIdAndDelete = jest.fn().mockResolvedValueOnce(mockPost); + jest.spyOn(Post, 'findByIdAndDelete').mockResolvedValueOnce(mockPost); await deletePostByIdHandler(req, res); @@ -355,10 +340,13 @@ describe('deletePostByIdHandler', () => { }); it('Delete Post: Failure - Post not found (Specified post ID is invalid)', async () => { - const req = createRequestObject({ params: { id: '6910293383' } }); + const req: any = createRequestObject({ params: { id: '6910293383' } }); // Mock the behavior of Post.findByIdAndRemove - Post.findByIdAndDelete = jest.fn().mockResolvedValueOnce(null); + + jest + .spyOn(Post, 'findByIdAndDelete') + .mockRejectedValueOnce(new Error(RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR)); await deletePostByIdHandler(req, res); @@ -367,11 +355,11 @@ describe('deletePostByIdHandler', () => { }); it('Delete Post: Failure - Internal Server Error', async () => { - const req = createRequestObject({ params: { id: '6910293383' } }); + const req: any = createRequestObject({ params: { id: '6910293383' } }); // Mock the behavior of Post.findByIdAndRemove - Post.findByIdAndDelete = jest - .fn() + jest + .spyOn(Post, 'findByIdAndDelete') .mockRejectedValueOnce(new Error(RESPONSE_MESSAGES.COMMON.INTERNAL_SERVER_ERROR)); await deletePostByIdHandler(req, res); diff --git a/backend/tests/utils/helper-objects.js b/backend/tests/utils/helper-objects.js deleted file mode 100644 index 551a8368..00000000 --- a/backend/tests/utils/helper-objects.js +++ /dev/null @@ -1,25 +0,0 @@ -import { validCategories } from '../../utils/constants'; -import { jest } from '@jest/globals'; -export const res = { - json: jest.fn(), - status: jest.fn().mockReturnThis(), -}; - -export const createPostObject = (options = {}) => { - return { - title: options.title || 'Test Post', - authorName: options.authorName || 'Test Author', - imageLink: options.imageLink || 'https://www.forTestingPurposeOnly/my-image.jpg', - categories: options.categories || [validCategories[0]], - description: options.description || 'This is a test post.', - isFeaturedPost: options.isFeaturedPost || false, - ...options, - }; -}; - -export const createRequestObject = (options = {}) => { - return { - body: options.body || {}, - params: options.params || {}, - }; -}; diff --git a/backend/tests/utils/helper-objects.ts b/backend/tests/utils/helper-objects.ts new file mode 100644 index 00000000..65eee208 --- /dev/null +++ b/backend/tests/utils/helper-objects.ts @@ -0,0 +1,51 @@ +import { validCategories } from '../../utils/constants'; +import { jest } from '@jest/globals'; + +import { Response } from 'express'; + +interface OptionsObject { + _id?: string; + title?: string; + authorName?: string; + imageLink?: string; + categories?: typeof validCategories; + description?: string; + isFeaturedPost?: boolean; +} + +export const createPostObject = (options: OptionsObject = {}): OptionsObject => { + return { + _id: options._id, + title: options.title || 'Test Post', + authorName: options.authorName || 'Test Author', + imageLink: options.imageLink || 'https://www.forTestingPurposeOnly/my-image.jpg', + categories: options.categories || [validCategories[0]], + description: options.description || 'This is a test post.', + isFeaturedPost: options.isFeaturedPost || false, + ...options, + }; +}; + +interface RequestOptions { + body?: Record; + params?: Record; +} + +export const createRequestObject = ( + options: RequestOptions = {} +): { body: Record; params: Record } => { + return { + body: options.body || {}, + params: options.params || {}, + }; +}; + +export type MockResponse = { + json: jest.Mock; + status: jest.Mock; +}; + +export const res: any = { + json: jest.fn().mockReturnThis() as unknown as Response, + status: jest.fn().mockReturnThis() as unknown as Response, +}; diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 00000000..e22655cc --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,108 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/backend/types/role-type.js b/backend/types/role-type.ts similarity index 100% rename from backend/types/role-type.js rename to backend/types/role-type.ts diff --git a/backend/types/types.d.ts b/backend/types/types.d.ts new file mode 100644 index 00000000..9755d4c8 --- /dev/null +++ b/backend/types/types.d.ts @@ -0,0 +1,8 @@ +declare namespace Express { + export interface Request { + user?: any; + } + export interface Response { + user?: any; + } +} diff --git a/backend/utils/api-error.js b/backend/utils/api-error.js deleted file mode 100644 index 260f5b0a..00000000 --- a/backend/utils/api-error.js +++ /dev/null @@ -1,20 +0,0 @@ -import { RESPONSE_MESSAGES } from './constants.js'; - -class ApiError extends Error { - constructor(status, message = RESPONSE_MESSAGES.COMMON.SOMETHING_WRONG, errors = [], stack = '') { - super(message); - this.status = status; - this.data = null; - this.message = message; - this.success = false; - this.errors = errors; - - if (stack) { - this.stack = stack; - } else { - Error.captureStackTrace(this, this.constructor); - } - } -} - -export { ApiError }; diff --git a/backend/utils/api-error.ts b/backend/utils/api-error.ts new file mode 100644 index 00000000..5ffa98f0 --- /dev/null +++ b/backend/utils/api-error.ts @@ -0,0 +1,37 @@ +import { Error as MongooseError } from 'mongoose'; +import { RESPONSE_MESSAGES } from './constants'; + +interface ApiErrorParams { + status: number; + message?: string; + errors?: any[]; + stack?: string; +} + +class ApiError extends MongooseError { + public status: number; + public data: null; + public success: boolean; + public errors: any[]; + constructor({ + status, + message = RESPONSE_MESSAGES.COMMON.SOMETHING_WRONG, + errors = [], + stack = '', + }: ApiErrorParams) { + super(message); + this.status = status; + this.data = null; + this.message = message; + this.success = false; + this.errors = errors; + + if (stack) { + this.stack = stack; + } else { + Error.captureStackTrace(this, this.constructor); + } + } +} + +export { ApiError }; diff --git a/backend/utils/api-response.js b/backend/utils/api-response.js deleted file mode 100644 index 3308bac8..00000000 --- a/backend/utils/api-response.js +++ /dev/null @@ -1,12 +0,0 @@ -import { HTTP_STATUS } from './constants.js'; - -class ApiResponse { - constructor(status, data, message = 'Success') { - this.status = status; - this.data = data; - this.message = message; - this.success = status < HTTP_STATUS.BAD_REQUEST; - } -} - -export { ApiResponse }; diff --git a/backend/utils/api-response.ts b/backend/utils/api-response.ts new file mode 100644 index 00000000..49e860be --- /dev/null +++ b/backend/utils/api-response.ts @@ -0,0 +1,17 @@ +import { HTTP_STATUS } from './constants'; + +class ApiResponse { + public status: number; + public data: any; + public message: string; + public success: boolean; + + constructor(status: number, data: any, message = 'Success') { + this.status = status; + this.data = data; + this.message = message; + this.success = status < HTTP_STATUS.BAD_REQUEST; + } +} + +export { ApiResponse }; diff --git a/backend/utils/async-handler.js b/backend/utils/async-handler.js deleted file mode 100644 index 6fe0277a..00000000 --- a/backend/utils/async-handler.js +++ /dev/null @@ -1,5 +0,0 @@ -export const asyncHandler = (func) => { - return (req, res, next) => { - Promise.resolve(func(req, res, next)).catch((err) => next(err)); - }; -}; diff --git a/backend/utils/async-handler.ts b/backend/utils/async-handler.ts new file mode 100644 index 00000000..ea267728 --- /dev/null +++ b/backend/utils/async-handler.ts @@ -0,0 +1,7 @@ +import { Request, Response, NextFunction } from 'express'; + +export const asyncHandler = (func: any) => { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(func(req, res, next)).catch((err) => next(err)); + }; +}; diff --git a/backend/utils/cache-posts.js b/backend/utils/cache-posts.ts similarity index 72% rename from backend/utils/cache-posts.js rename to backend/utils/cache-posts.ts index fa2d31c5..2261c0ab 100644 --- a/backend/utils/cache-posts.js +++ b/backend/utils/cache-posts.ts @@ -1,12 +1,12 @@ -import { getRedisClient } from '../services/redis.js'; -import { REDIS_PREFIX } from './constants.js'; +import { getRedisClient } from '../services/redis'; +import { REDIS_PREFIX } from './constants'; // Helper function to check if Redis is available function isRedisEnabled() { return getRedisClient() !== null; } -export async function retrieveDataFromCache(key) { +export async function retrieveDataFromCache(key: string) { if (!isRedisEnabled()) return null; // Skip cache if Redis is not available const cacheKey = `${REDIS_PREFIX}:${key}`; @@ -17,14 +17,14 @@ export async function retrieveDataFromCache(key) { return null; } -export async function storeDataInCache(key, data) { +export async function storeDataInCache(key: string, data: any) { if (!isRedisEnabled()) return; // Skip cache if Redis is not available const cacheKey = `${REDIS_PREFIX}:${key}`; await getRedisClient().set(cacheKey, JSON.stringify(data)); } -export async function deleteDataFromCache(key) { +export async function deleteDataFromCache(key: string) { if (!isRedisEnabled()) return; // Skip cache if Redis is not available const cacheKey = `${REDIS_PREFIX}:${key}`; diff --git a/backend/utils/constants.js b/backend/utils/constants.ts similarity index 100% rename from backend/utils/constants.js rename to backend/utils/constants.ts diff --git a/backend/utils/cookie_options.js b/backend/utils/cookie_options.js deleted file mode 100644 index d8779b67..00000000 --- a/backend/utils/cookie_options.js +++ /dev/null @@ -1,8 +0,0 @@ -import { ACCESS_COOKIE_MAXAGE, NODE_ENV } from '../config/utils.js'; - -export const cookieOptions = { - httpOnly: true, - sameSite: NODE_ENV === 'Development' ? 'lax' : 'none', - secure: NODE_ENV === 'Development' ? false : true, - maxAge: ACCESS_COOKIE_MAXAGE, -}; diff --git a/backend/utils/cookie_options.ts b/backend/utils/cookie_options.ts new file mode 100644 index 00000000..6561e246 --- /dev/null +++ b/backend/utils/cookie_options.ts @@ -0,0 +1,18 @@ +import { ACCESS_COOKIE_MAXAGE, NODE_ENV } from '../config/utils'; +const defaultMaxAge = 3600000; +interface CookieObject { + httpOnly: boolean; + sameSite: 'lax' | 'strict' | 'none'; + secure: boolean; + maxAge?: number | undefined; +} +const maxAge = + typeof ACCESS_COOKIE_MAXAGE === 'string' ? parseInt(ACCESS_COOKIE_MAXAGE, 10) : defaultMaxAge; + +const validMaxAge = isNaN(maxAge) ? defaultMaxAge : maxAge; +export const cookieOptions: CookieObject = { + httpOnly: true, + sameSite: NODE_ENV === 'Development' ? 'lax' : 'none', + secure: NODE_ENV === 'Development' ? false : true, + maxAge: validMaxAge, +}; diff --git a/backend/utils/middleware.js b/backend/utils/middleware.js deleted file mode 100644 index d20128d7..00000000 --- a/backend/utils/middleware.js +++ /dev/null @@ -1,15 +0,0 @@ -import { retrieveDataFromCache } from './cache-posts.js'; -import { HTTP_STATUS } from './constants.js'; - -export const cacheHandler = (key) => async (req, res, next) => { - try { - const cachedData = await retrieveDataFromCache(key); - if (cachedData) { - console.log(`Getting cached data for key: ${key}`); - return res.status(HTTP_STATUS.OK).json(cachedData); - } - next(); // Proceed to the route handler if data is not cached - } catch (err) { - res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); - } -}; diff --git a/backend/utils/middleware.ts b/backend/utils/middleware.ts new file mode 100644 index 00000000..1c218170 --- /dev/null +++ b/backend/utils/middleware.ts @@ -0,0 +1,17 @@ +import { retrieveDataFromCache } from './cache-posts'; +import { HTTP_STATUS } from './constants'; +import { Request, Response, NextFunction } from 'express'; + +export const cacheHandler = + (key: string) => async (req: Request, res: Response, next: NextFunction) => { + try { + const cachedData = await retrieveDataFromCache(key); + if (cachedData) { + console.log(`Getting cached data for key: ${key}`); + return res.status(HTTP_STATUS.OK).json(cachedData); + } + next(); // Proceed to the route handler if data is not cached + } catch (err: any) { + res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); + } + }; From 4a7d1595a5d4ec565056732fa6811417a9ee47fe Mon Sep 17 00:00:00 2001 From: Ameer jafar Date: Mon, 19 Aug 2024 19:46:28 +0530 Subject: [PATCH 2/6] feat-#3: made the login, signup and add-blog workflow correctly --- backend/app.ts | 2 +- backend/controllers/posts-controller.ts | 9 ++++----- backend/middlewares/auth-middleware.ts | 8 ++++---- backend/models/user.ts | 1 - backend/package.json | 2 ++ backend/services/redis.ts | 3 +-- backend/types/types.d.ts | 4 ++-- backend/utils/cache-posts.ts | 11 ++++++----- backend/utils/cookie_options.ts | 2 +- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/backend/app.ts b/backend/app.ts index bfd59416..1de188bf 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -15,7 +15,7 @@ const app = express(); app.use( cors({ // added origin - origin: ['http://localhost:3000'], + origin: [FRONTEND_URL as string, 'http://localhost:3000'], credentials: true, }) ); diff --git a/backend/controllers/posts-controller.ts b/backend/controllers/posts-controller.ts index de88debb..c1d2246a 100644 --- a/backend/controllers/posts-controller.ts +++ b/backend/controllers/posts-controller.ts @@ -13,7 +13,6 @@ export const createPostHandler = async (req: Request, res: Response) => { description, isFeaturedPost = false, } = req.body; - const userId = req.user._id; // Validation - check if all fields are filled @@ -50,9 +49,9 @@ export const createPostHandler = async (req: Request, res: Response) => { const [savedPost] = await Promise.all([ post.save(), // Save the post - deleteDataFromCache(REDIS_KEYS.ALL_POSTS), // Invalidate cache for all posts - deleteDataFromCache(REDIS_KEYS.FEATURED_POSTS), // Invalidate cache for featured posts - deleteDataFromCache(REDIS_KEYS.LATEST_POSTS), // Invalidate cache for latest posts + // deleteDataFromCache(REDIS_KEYS.ALL_POSTS), // Invalidate cache for all posts + // deleteDataFromCache(REDIS_KEYS.FEATURED_POSTS), // Invalidate cache for featured posts + // deleteDataFromCache(REDIS_KEYS.LATEST_POSTS), // Invalidate cache for latest posts ]); // updating user doc to include the ObjectId of the created post @@ -60,7 +59,7 @@ export const createPostHandler = async (req: Request, res: Response) => { res.status(HTTP_STATUS.OK).json(savedPost); } catch (err: any) { - res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err.message }); + res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ message: err }); } }; diff --git a/backend/middlewares/auth-middleware.ts b/backend/middlewares/auth-middleware.ts index a55c2524..1d392f5b 100644 --- a/backend/middlewares/auth-middleware.ts +++ b/backend/middlewares/auth-middleware.ts @@ -6,13 +6,14 @@ import { Role } from '../types/role-type'; import User from '../models/user'; import { Request, Response, NextFunction } from 'express'; +import { ObjectId } from 'mongoose'; interface JwtPayload { - _id: string; + _id: ObjectId; } export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => { - const token = req.cookies?.access_token; + const token = await req.cookies.access_token; if (!token) { return next( new ApiError({ @@ -24,11 +25,10 @@ export const authMiddleware = async (req: Request, res: Response, next: NextFunc try { const { _id } = jwt.verify(token, JWT_SECRET as string) as JwtPayload; - req.user = await User.findById(_id); next(); } catch (error: any) { - console.log('Token verification error:', error.message); + console.log('Token verification error:', error); return next( new ApiError({ status: HTTP_STATUS.FORBIDDEN, diff --git a/backend/models/user.ts b/backend/models/user.ts index ec0ea110..b3e07eb9 100644 --- a/backend/models/user.ts +++ b/backend/models/user.ts @@ -6,7 +6,6 @@ import { ACCESS_TOKEN_EXPIRES_IN, JWT_SECRET, REFRESH_TOKEN_EXPIRES_IN } from '. import { Role } from '../types/role-type'; interface UserObject extends Document { - id: number; userName: string; fullName: string; email: string; diff --git a/backend/package.json b/backend/package.json index 58f5e0d9..90894bdf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,9 +8,11 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/express-session": "^1.18.0", + "@types/ioredis": "^5.0.0", "@types/jsonwebtoken": "^9.0.6", "@types/passport": "^1.0.16", "@types/passport-google-oauth20": "^2.0.16", + "@types/redis": "^4.0.11", "@types/supertest": "^6.0.2", "axios": "^1.6.8", "bcryptjs": "^2.4.3", diff --git a/backend/services/redis.ts b/backend/services/redis.ts index e121e009..5ebb4de2 100644 --- a/backend/services/redis.ts +++ b/backend/services/redis.ts @@ -2,7 +2,6 @@ import { createClient } from 'redis'; import { REDIS_URL } from '../config/utils'; let redis: any = null; - export async function connectToRedis() { try { if (REDIS_URL) { @@ -19,6 +18,6 @@ export async function connectToRedis() { } } -export function getRedisClient() { +export function getRedisClient(): any { return redis; } diff --git a/backend/types/types.d.ts b/backend/types/types.d.ts index 9755d4c8..1b3a1916 100644 --- a/backend/types/types.d.ts +++ b/backend/types/types.d.ts @@ -1,8 +1,8 @@ declare namespace Express { export interface Request { - user?: any; + user: any; } export interface Response { - user?: any; + user: any; } } diff --git a/backend/utils/cache-posts.ts b/backend/utils/cache-posts.ts index 2261c0ab..c4247f71 100644 --- a/backend/utils/cache-posts.ts +++ b/backend/utils/cache-posts.ts @@ -6,7 +6,7 @@ function isRedisEnabled() { return getRedisClient() !== null; } -export async function retrieveDataFromCache(key: string) { +export async function retrieveDataFromCache(key: any) { if (!isRedisEnabled()) return null; // Skip cache if Redis is not available const cacheKey = `${REDIS_PREFIX}:${key}`; @@ -17,16 +17,17 @@ export async function retrieveDataFromCache(key: string) { return null; } -export async function storeDataInCache(key: string, data: any) { +export async function storeDataInCache(key: any, data: any) { if (!isRedisEnabled()) return; // Skip cache if Redis is not available const cacheKey = `${REDIS_PREFIX}:${key}`; await getRedisClient().set(cacheKey, JSON.stringify(data)); } -export async function deleteDataFromCache(key: string) { +export async function deleteDataFromCache(key: any): Promise { if (!isRedisEnabled()) return; // Skip cache if Redis is not available - const cacheKey = `${REDIS_PREFIX}:${key}`; - await getRedisClient().del(cacheKey); + if (getRedisClient().exists(cacheKey)) { + await getRedisClient().del(cacheKey); + } } diff --git a/backend/utils/cookie_options.ts b/backend/utils/cookie_options.ts index 6561e246..0b0d146f 100644 --- a/backend/utils/cookie_options.ts +++ b/backend/utils/cookie_options.ts @@ -4,7 +4,7 @@ interface CookieObject { httpOnly: boolean; sameSite: 'lax' | 'strict' | 'none'; secure: boolean; - maxAge?: number | undefined; + maxAge: number; } const maxAge = typeof ACCESS_COOKIE_MAXAGE === 'string' ? parseInt(ACCESS_COOKIE_MAXAGE, 10) : defaultMaxAge; From 997050832c07e41a84f161f592ffb572fc167f54 Mon Sep 17 00:00:00 2001 From: Ameer jafar Date: Sat, 24 Aug 2024 14:05:24 +0530 Subject: [PATCH 3/6] feat: cleared the unwanted line in the tsconfig.json file --- backend/tsconfig.json | 112 +++--------------------------------------- 1 file changed, 8 insertions(+), 104 deletions(-) diff --git a/backend/tsconfig.json b/backend/tsconfig.json index e22655cc..566cea52 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,108 +1,12 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "target": "es2016", + "module": "commonjs", + "allowArbitraryExtensions": true, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true } } From 98946649db00f6bc8923e13cc4fb882850a06ee1 Mon Sep 17 00:00:00 2001 From: Ameer jafar Date: Sat, 24 Aug 2024 14:11:39 +0530 Subject: [PATCH 4/6] feat: remove the command line from the deleteDataFromCache() function --- backend/controllers/posts-controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/controllers/posts-controller.ts b/backend/controllers/posts-controller.ts index c1d2246a..c8eebd64 100644 --- a/backend/controllers/posts-controller.ts +++ b/backend/controllers/posts-controller.ts @@ -49,9 +49,9 @@ export const createPostHandler = async (req: Request, res: Response) => { const [savedPost] = await Promise.all([ post.save(), // Save the post - // deleteDataFromCache(REDIS_KEYS.ALL_POSTS), // Invalidate cache for all posts - // deleteDataFromCache(REDIS_KEYS.FEATURED_POSTS), // Invalidate cache for featured posts - // deleteDataFromCache(REDIS_KEYS.LATEST_POSTS), // Invalidate cache for latest posts + deleteDataFromCache(REDIS_KEYS.ALL_POSTS), // Invalidate cache for all posts + deleteDataFromCache(REDIS_KEYS.FEATURED_POSTS), // Invalidate cache for featured posts + deleteDataFromCache(REDIS_KEYS.LATEST_POSTS), // Invalidate cache for latest posts ]); // updating user doc to include the ObjectId of the created post From 00a18877911c4c549e3f328e79e81d674e1da625 Mon Sep 17 00:00:00 2001 From: Ameer jafar Date: Tue, 27 Aug 2024 19:14:56 +0530 Subject: [PATCH 5/6] feat: changed the imported file extension to .js --- frontend/.eslintrc.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 74848a50..5d324d74 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -1,4 +1,4 @@ -module.exports = { +export default { root: true, env: { browser: true, es2020: true }, extends: [ From ee63d7741ac27350ae7523f419182720c536e945 Mon Sep 17 00:00:00 2001 From: Ameer jafar Date: Tue, 27 Aug 2024 19:15:48 +0530 Subject: [PATCH 6/6] feat: changed the imported file extension to .js --- backend/api/index.ts | 2 +- backend/app.ts | 12 ++++++------ backend/config/db.ts | 2 +- backend/config/passport.ts | 2 +- backend/controllers/auth-controller.ts | 14 +++++++------- backend/controllers/posts-controller.ts | 8 ++++---- backend/controllers/user-controller.ts | 6 +++--- backend/middlewares/auth-middleware.ts | 10 +++++----- backend/middlewares/error-middleware.ts | 2 +- backend/middlewares/post-middleware.ts | 4 ++-- backend/models/user.ts | 4 ++-- backend/package.json | 7 +++++-- backend/routes/auth.ts | 6 +++--- backend/routes/posts.ts | 6 +++--- backend/routes/user.ts | 4 ++-- backend/server.ts | 8 ++++---- backend/services/redis.ts | 2 +- .../controllers/posts-controller.test.ts | 7 +++---- .../unit/controllers/posts-controller.test.ts | 8 ++++---- backend/tests/utils/helper-objects.ts | 2 +- backend/tsconfig.json | 5 +++-- backend/utils/api-error.ts | 2 +- backend/utils/api-response.ts | 2 +- backend/utils/cache-posts.ts | 4 ++-- backend/utils/cookie_options.ts | 2 +- backend/utils/middleware.ts | 4 ++-- 26 files changed, 69 insertions(+), 66 deletions(-) diff --git a/backend/api/index.ts b/backend/api/index.ts index d5d94437..35b79442 100644 --- a/backend/api/index.ts +++ b/backend/api/index.ts @@ -1,2 +1,2 @@ -import app from '../app'; +import app from '../app.js'; export default app; diff --git a/backend/app.ts b/backend/app.ts index 1de188bf..1443969a 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -2,13 +2,13 @@ import compression from 'compression'; import cookieParser from 'cookie-parser'; import cors from 'cors'; import express from 'express'; -import authRouter from './routes/auth'; -import postsRouter from './routes/posts'; -import userRouter from './routes/user'; -import errorMiddleware from './middlewares/error-middleware'; -import passport from './config/passport'; +import authRouter from './routes/auth.js'; +import postsRouter from './routes/posts.js'; +import userRouter from './routes/user.js'; +import errorMiddleware from './middlewares/error-middleware.js'; +import passport from './config/passport.js'; import session from 'express-session'; -import { FRONTEND_URL } from './config/utils'; +import { FRONTEND_URL } from './config/utils.js'; const app = express(); diff --git a/backend/config/db.ts b/backend/config/db.ts index ea0545c8..1cddf757 100644 --- a/backend/config/db.ts +++ b/backend/config/db.ts @@ -1,5 +1,5 @@ import mongoose, { AnyArray } from 'mongoose'; -import { MONGODB_URI } from './utils'; +import { MONGODB_URI } from './utils.js'; export default async function connectDB() { try { diff --git a/backend/config/passport.ts b/backend/config/passport.ts index 9dd6bcc9..db8314ca 100644 --- a/backend/config/passport.ts +++ b/backend/config/passport.ts @@ -1,6 +1,6 @@ import passport from 'passport'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -import User from '../models/user'; +import User from '../models/user.js'; passport.use( new GoogleStrategy( { diff --git a/backend/controllers/auth-controller.ts b/backend/controllers/auth-controller.ts index 2e7e375f..38cc9ecf 100644 --- a/backend/controllers/auth-controller.ts +++ b/backend/controllers/auth-controller.ts @@ -1,11 +1,11 @@ -import User from '../models/user'; +import User from '../models/user.js'; import jwt from 'jsonwebtoken'; -import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants'; -import { cookieOptions } from '../utils/cookie_options'; -import { JWT_SECRET } from '../config/utils'; -import { ApiError } from '../utils/api-error'; -import { ApiResponse } from '../utils/api-response'; -import { asyncHandler } from '../utils/async-handler'; +import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js'; +import { cookieOptions } from '../utils/cookie_options.js'; +import { JWT_SECRET } from '../config/utils.js'; +import { ApiError } from '../utils/api-error.js'; +import { ApiResponse } from '../utils/api-response.js'; +import { asyncHandler } from '../utils/async-handler.js'; import { NextFunction, Request, Response } from 'express'; //REGULAR EMAIL PASSWORD STRATEGY diff --git a/backend/controllers/posts-controller.ts b/backend/controllers/posts-controller.ts index c8eebd64..562d12fc 100644 --- a/backend/controllers/posts-controller.ts +++ b/backend/controllers/posts-controller.ts @@ -1,7 +1,7 @@ -import Post from '../models/post'; -import User from '../models/user'; -import { deleteDataFromCache, storeDataInCache } from '../utils/cache-posts'; -import { HTTP_STATUS, REDIS_KEYS, RESPONSE_MESSAGES, validCategories } from '../utils/constants'; +import Post from '../models/post.js'; +import User from '../models/user.js'; +import { deleteDataFromCache, storeDataInCache } from '../utils/cache-posts.js'; +import { HTTP_STATUS, REDIS_KEYS, RESPONSE_MESSAGES, validCategories } from '../utils/constants.js'; import { Request, Response, NextFunction } from 'express'; export const createPostHandler = async (req: Request, res: Response) => { try { diff --git a/backend/controllers/user-controller.ts b/backend/controllers/user-controller.ts index dfda9028..ebb62e38 100644 --- a/backend/controllers/user-controller.ts +++ b/backend/controllers/user-controller.ts @@ -1,6 +1,6 @@ -import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants'; -import User from '../models/user'; -import { Role } from '../types/role-type'; +import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js'; +import User from '../models/user.js'; +import { Role } from '../types/role-type.js'; import { Request, Response } from 'express'; export const getAllUserHandler = async (req: Request, res: Response) => { diff --git a/backend/middlewares/auth-middleware.ts b/backend/middlewares/auth-middleware.ts index 1d392f5b..f9f0bb51 100644 --- a/backend/middlewares/auth-middleware.ts +++ b/backend/middlewares/auth-middleware.ts @@ -1,9 +1,9 @@ -import { JWT_SECRET } from '../config/utils'; -import { ApiError } from '../utils/api-error'; -import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants'; +import { JWT_SECRET } from '../config/utils.js'; +import { ApiError } from '../utils/api-error.js'; +import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js'; import jwt from 'jsonwebtoken'; -import { Role } from '../types/role-type'; -import User from '../models/user'; +import { Role } from '../types/role-type.js'; +import User from '../models/user.js'; import { Request, Response, NextFunction } from 'express'; import { ObjectId } from 'mongoose'; diff --git a/backend/middlewares/error-middleware.ts b/backend/middlewares/error-middleware.ts index dcacb2c8..ead7b267 100644 --- a/backend/middlewares/error-middleware.ts +++ b/backend/middlewares/error-middleware.ts @@ -1,4 +1,4 @@ -import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants'; +import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js'; import { Request, Response, NextFunction } from 'express'; const errorMiddleware = (err: any, req: Request, res: Response, next: NextFunction) => { diff --git a/backend/middlewares/post-middleware.ts b/backend/middlewares/post-middleware.ts index b214343f..2bcb4a70 100644 --- a/backend/middlewares/post-middleware.ts +++ b/backend/middlewares/post-middleware.ts @@ -1,5 +1,5 @@ -import Post from '../models/post'; -import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants'; +import Post from '../models/post.js'; +import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js'; import { Request, Response, NextFunction } from 'express'; export const isAuthorMiddleware = async (req: Request, res: Response, next: NextFunction) => { diff --git a/backend/models/user.ts b/backend/models/user.ts index b3e07eb9..07d5c2c7 100644 --- a/backend/models/user.ts +++ b/backend/models/user.ts @@ -2,8 +2,8 @@ import { Schema, model, Document } from 'mongoose'; import JWT from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; import crypto from 'crypto'; -import { ACCESS_TOKEN_EXPIRES_IN, JWT_SECRET, REFRESH_TOKEN_EXPIRES_IN } from '../config/utils'; -import { Role } from '../types/role-type'; +import { ACCESS_TOKEN_EXPIRES_IN, JWT_SECRET, REFRESH_TOKEN_EXPIRES_IN } from '../config/utils.js'; +import { Role } from '../types/role-type.js'; interface UserObject extends Document { userName: string; diff --git a/backend/package.json b/backend/package.json index 90894bdf..f5f045d3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,5 +1,5 @@ { - "type": "commonjs", + "type": "module", "dependencies": { "@types/bcrypt": "^5.0.2", "@types/bcryptjs": "^2.4.6", @@ -10,6 +10,7 @@ "@types/express-session": "^1.18.0", "@types/ioredis": "^5.0.0", "@types/jsonwebtoken": "^9.0.6", + "@types/mongoose": "^5.11.97", "@types/passport": "^1.0.16", "@types/passport-google-oauth20": "^2.0.16", "@types/redis": "^4.0.11", @@ -44,7 +45,7 @@ "vercel-build": "echo yay" }, "jest": { - "globalTeardown": "./tests/teardown.js", + "globalTeardown": "./tests/teardown", "transform": { "^.+\\.js?$": "babel-jest" } @@ -52,8 +53,10 @@ "devDependencies": { "@babel/preset-env": "^7.23.2", "@eslint/js": "^9.9.0", + "@jest/globals": "^29.7.0", "@stylistic/eslint-plugin-js": "^2.1.0", "@types/eslint": "^9.6.0", + "@types/jest": "^29.5.12", "@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/parser": "^8.1.0", "babel-jest": "^29.7.0", diff --git a/backend/routes/auth.ts b/backend/routes/auth.ts index a63e34c6..ba57edec 100644 --- a/backend/routes/auth.ts +++ b/backend/routes/auth.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; -import { authMiddleware } from '../middlewares/auth-middleware'; -import passport from '../config/passport'; +import { authMiddleware } from '../middlewares/auth-middleware.js'; +import passport from '../config/passport.js'; import jwt from 'jsonwebtoken'; import { Request, Response } from 'express'; import { @@ -8,7 +8,7 @@ import { signInWithEmailOrUsername, signOutUser, isLoggedIn, -} from '../controllers/auth-controller'; +} from '../controllers/auth-controller.js'; const router = Router(); diff --git a/backend/routes/posts.ts b/backend/routes/posts.ts index 50f908d9..bd393ccd 100644 --- a/backend/routes/posts.ts +++ b/backend/routes/posts.ts @@ -10,9 +10,9 @@ import { getRelatedPostsByCategories, updatePostHandler, } from '../controllers/posts-controller.js'; -import { REDIS_KEYS } from '../utils/constants'; -import { cacheHandler } from '../utils/middleware'; -import { isAdminMiddleware, authMiddleware } from '../middlewares/auth-middleware'; +import { REDIS_KEYS } from '../utils/constants.js'; +import { cacheHandler } from '../utils/middleware.js'; +import { isAdminMiddleware, authMiddleware } from '../middlewares/auth-middleware.js'; import { isAuthorMiddleware } from '../middlewares/post-middleware.js'; const router = Router(); diff --git a/backend/routes/user.ts b/backend/routes/user.ts index 1caa2e63..73ba4a7a 100644 --- a/backend/routes/user.ts +++ b/backend/routes/user.ts @@ -1,10 +1,10 @@ import { Router } from 'express'; -import { isAdminMiddleware, authMiddleware } from '../middlewares/auth-middleware'; +import { isAdminMiddleware, authMiddleware } from '../middlewares/auth-middleware.js'; import { changeUserRoleHandler, deleteUserHandler, getAllUserHandler, -} from '../controllers/user-controller'; +} from '../controllers/user-controller.js'; const router = Router(); diff --git a/backend/server.ts b/backend/server.ts index 4afe582f..384e7448 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -1,12 +1,12 @@ -import app from './app'; -import connectDB from './config/db'; -import { PORT } from './config/utils'; +import app from './app.js'; +import connectDB from './config/db.js'; +import { PORT } from './config/utils.js'; import { connectToRedis } from './services/redis.js'; const server = () => { const port = PORT || 8080; - // Redis connection + // Redis connections connectToRedis(); // mongodb connection connectDB() diff --git a/backend/services/redis.ts b/backend/services/redis.ts index 5ebb4de2..ef4c48d9 100644 --- a/backend/services/redis.ts +++ b/backend/services/redis.ts @@ -1,5 +1,5 @@ import { createClient } from 'redis'; -import { REDIS_URL } from '../config/utils'; +import { REDIS_URL } from '../config/utils.js'; let redis: any = null; export async function connectToRedis() { diff --git a/backend/tests/integration/controllers/posts-controller.test.ts b/backend/tests/integration/controllers/posts-controller.test.ts index 8e44d24e..db8f045b 100644 --- a/backend/tests/integration/controllers/posts-controller.test.ts +++ b/backend/tests/integration/controllers/posts-controller.test.ts @@ -1,11 +1,10 @@ import mongoose from 'mongoose'; import request from 'supertest'; -import Post from '../../../models/post'; +import Post from '../../../models/post.js'; import server from '../../../server'; -import { validCategories, HTTP_STATUS, RESPONSE_MESSAGES } from '../../../utils/constants'; -import { createPostObject } from '../../utils/helper-objects'; +import { validCategories, HTTP_STATUS, RESPONSE_MESSAGES } from '../../../utils/constants.js'; +import { createPostObject } from '../../utils/helper-objects.js'; import { expect, jest, it, afterAll, describe } from '@jest/globals'; - afterAll(async () => { await mongoose.disconnect(); }); diff --git a/backend/tests/unit/controllers/posts-controller.test.ts b/backend/tests/unit/controllers/posts-controller.test.ts index 9b0a40b4..07ee7731 100644 --- a/backend/tests/unit/controllers/posts-controller.test.ts +++ b/backend/tests/unit/controllers/posts-controller.test.ts @@ -7,11 +7,11 @@ import { getPostByCategoryHandler, getPostByIdHandler, updatePostHandler, -} from '../../../controllers/posts-controller'; -import Post from '../../../models/post'; +} from '../../../controllers/posts-controller.js'; +import Post from '../../../models/post.js'; import { expect, jest, it, describe } from '@jest/globals'; -import { validCategories, HTTP_STATUS, RESPONSE_MESSAGES } from '../../../utils/constants'; -import { createPostObject, createRequestObject, res } from '../../utils/helper-objects'; +import { validCategories, HTTP_STATUS, RESPONSE_MESSAGES } from '../../../utils/constants.js'; +import { createPostObject, createRequestObject, res } from '../../utils/helper-objects.js'; jest.mock('../../../models/post.js', () => ({ __esModule: true, diff --git a/backend/tests/utils/helper-objects.ts b/backend/tests/utils/helper-objects.ts index 65eee208..e75abd34 100644 --- a/backend/tests/utils/helper-objects.ts +++ b/backend/tests/utils/helper-objects.ts @@ -1,4 +1,4 @@ -import { validCategories } from '../../utils/constants'; +import { validCategories } from '../../utils/constants.js'; import { jest } from '@jest/globals'; import { Response } from 'express'; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 566cea52..5ad7421b 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,12 +1,13 @@ { "compilerOptions": { "target": "es2016", - "module": "commonjs", + "module": "ESNext", + "moduleResolution": "Node", "allowArbitraryExtensions": true, "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, - "skipLibCheck": true + "skipLibCheck": true, } } diff --git a/backend/utils/api-error.ts b/backend/utils/api-error.ts index 5ffa98f0..e20017e9 100644 --- a/backend/utils/api-error.ts +++ b/backend/utils/api-error.ts @@ -1,5 +1,5 @@ import { Error as MongooseError } from 'mongoose'; -import { RESPONSE_MESSAGES } from './constants'; +import { RESPONSE_MESSAGES } from './constants.js'; interface ApiErrorParams { status: number; diff --git a/backend/utils/api-response.ts b/backend/utils/api-response.ts index 49e860be..b66e7da5 100644 --- a/backend/utils/api-response.ts +++ b/backend/utils/api-response.ts @@ -1,4 +1,4 @@ -import { HTTP_STATUS } from './constants'; +import { HTTP_STATUS } from './constants.js'; class ApiResponse { public status: number; diff --git a/backend/utils/cache-posts.ts b/backend/utils/cache-posts.ts index c4247f71..96ac114c 100644 --- a/backend/utils/cache-posts.ts +++ b/backend/utils/cache-posts.ts @@ -1,5 +1,5 @@ -import { getRedisClient } from '../services/redis'; -import { REDIS_PREFIX } from './constants'; +import { getRedisClient } from '../services/redis.js'; +import { REDIS_PREFIX } from './constants.js'; // Helper function to check if Redis is available function isRedisEnabled() { diff --git a/backend/utils/cookie_options.ts b/backend/utils/cookie_options.ts index 0b0d146f..876b78d8 100644 --- a/backend/utils/cookie_options.ts +++ b/backend/utils/cookie_options.ts @@ -1,4 +1,4 @@ -import { ACCESS_COOKIE_MAXAGE, NODE_ENV } from '../config/utils'; +import { ACCESS_COOKIE_MAXAGE, NODE_ENV } from '../config/utils.js'; const defaultMaxAge = 3600000; interface CookieObject { httpOnly: boolean; diff --git a/backend/utils/middleware.ts b/backend/utils/middleware.ts index 1c218170..e97c0651 100644 --- a/backend/utils/middleware.ts +++ b/backend/utils/middleware.ts @@ -1,5 +1,5 @@ -import { retrieveDataFromCache } from './cache-posts'; -import { HTTP_STATUS } from './constants'; +import { retrieveDataFromCache } from './cache-posts.js'; +import { HTTP_STATUS } from './constants.js'; import { Request, Response, NextFunction } from 'express'; export const cacheHandler =