diff --git a/__tests__/email.test.ts b/__tests__/email.test.ts new file mode 100644 index 0000000..b42e035 --- /dev/null +++ b/__tests__/email.test.ts @@ -0,0 +1,61 @@ +import { IEmail } from '../src/interfaces/email'; + +function isValidEmail(email: IEmail): boolean { + return ( + typeof email.from === 'string' && + typeof email.to === 'string' && + typeof email.subject === 'string' && + typeof email.text === 'string' && + (email.cc === undefined || typeof email.cc === 'string') && + (email.bcc === undefined || typeof email.bcc === 'string') && + (email.html === undefined || typeof email.html === 'string') + ); +} + +describe('IEmail Interface', () => { + test('should accept a valid email object', () => { + const validEmail: IEmail = { + from: 'test@example.com', + to: 'recipient@example.com', + subject: 'Test Subject', + text: 'Test message body', + }; + + expect(isValidEmail(validEmail)).toBe(true); + }); + + test('should accept an email with optional fields', () => { + const validEmailWithOptionalFields: IEmail = { + from: 'test@example.com', + to: 'recipient@example.com', + subject: 'Test Subject', + text: 'Test message body', + cc: 'cc@example.com', + bcc: 'bcc@example.com', + html: '
Test message body
', + }; + + expect(isValidEmail(validEmailWithOptionalFields)).toBe(true); + }); + + test('should reject an email without required fields', () => { + const invalidEmail: any = { + from: 'test@example.com', + subject: 'Test Subject', + text: 'Test message body', + }; + + expect(isValidEmail(invalidEmail)).toBe(false); + }); + + test('should reject an email with invalid types', () => { + const invalidEmail: any = { + from: 'test@example.com', + to: 123, + subject: 'Test Subject', + text: 'Test message body', + }; + + expect(isValidEmail(invalidEmail)).toBe(false); + }); +}); diff --git a/__tests__/express.test.ts b/__tests__/express.test.ts new file mode 100644 index 0000000..4518cce --- /dev/null +++ b/__tests__/express.test.ts @@ -0,0 +1,51 @@ +import { APIRequest, APIResponse } from '../src/interfaces/express'; +import { Request, Response } from 'express'; + +describe('APIRequest and APIResponse Interfaces', () => { + it('should correctly type the body of APIRequest', () => { + interface ExampleBody { + testName: string; + age: number; + } + + const req: APIRequest+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 | 1x + +1x +1x + + + + + +1x + + + + + + + + + + + + + + + + + + + + + + + +1x +1x + + + +1x + + + + + + + + + + + + + +1x + | import Joi from 'joi';
+
+import { config as dotenvConfig } from 'dotenv';
+dotenvConfig({
+ path: ['.env.local', '.env']
+});
+
+
+// define validation for all the env vars
+const envVarsSchema = Joi.object({
+ NODE_ENV: Joi.string()
+ .allow('development', 'production', 'test', 'provision')
+ .default(process.env.NODE_ENV),
+ SERVER_PORT: Joi.number().default(process.env.SERVER_PORT),
+ // MONGOOSE_DEBUG: Joi.boolean().when('NODE_ENV', {
+ // is: Joi.string().equal('development'),
+ // then: Joi.boolean().default(true),
+ // otherwise: Joi.boolean().default(false),
+ // }),
+ JWT_SECRET: Joi.string().default(process.env.JWT_SECRET)
+ .required()
+ .description('JWT Secret required to sign'),
+ DB_CONN_STRING: Joi.string().default(process.env.DB_CONN_STRING),
+ DB_NAME: Joi.string().default(process.env.DB_NAME),
+ SMTP_HOST: Joi.string().default(process.env.SMTP_HOST),
+ SMTP_PORT: Joi.number().default(process.env.SMTP_PORT),
+ SMTP_USERNAME: Joi.string().default(process.env.SMTP_USERNAME),
+ SMTP_PASSWORD: Joi.string().default(process.env.SMTP_PASSWORD),
+ SMTP_SENDER: Joi.string().default(process.env.SMTP_SENDER)
+})
+ .unknown()
+ .required();
+
+const { error, value: envVars } = envVarsSchema.validate(process.env);
+if (error) {
+ //throw new Error(`Config validation error: ${error.message}`);
+}
+
+const config = {
+ env: envVars.NODE_ENV,
+ port: envVars.SERVER_PORT,
+ // mongooseDebug: envVars.MONGOOSE_DEBUG,
+ jwtSecret: envVars.JWT_SECRET,
+ dbConnString: envVars.DB_CONN_STRING,
+ dbName: envVars.DB_NAME,
+ smtpHost: envVars.SMTP_HOST,
+ smtpPort: envVars.SMTP_PORT,
+ smtpUsername: envVars.SMTP_USERNAME,
+ smtpPassword: envVars.SMTP_PASSWORD,
+ smtpSender: envVars.SMTP_SENDER
+};
+
+export default config;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| config.ts | +
+
+ |
+ 100% | +8/8 | +100% | +1/1 | +100% | +0/0 | +100% | +8/8 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| userRoles.ts | +
+
+ |
+ 100% | +1/1 | +100% | +0/0 | +100% | +0/0 | +100% | +1/1 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 | 1x + + + + + + + | export const UserRole = {
+ ADMIN: 'admin',
+ visitor: 'user',
+ GUEST: 'guest'
+} as const;
+
+export type TUserRole = typeof UserRole[keyof typeof UserRole];
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 | 1x + +1x +1x +1x +1x + + +1x + + + + + + + + + + + + + + + + + + + + + + + + +1x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1x + + + + + + + + + + + + + + + + + + + | import { createUser } from '../../src/controllers/user.controller';
+import { Request, Response } from 'express';
+import bcrypt from 'bcrypt';
+import status from 'http-status';
+import User from '../models/user.model';
+import handleErrorResponse from '../utils/controller.helper';
+import { IUserSignIn, IUserSignOut, IUserSignUp } from '@interfaces/user';
+
+export async function signUp(
+ req: Request<Record<string, never>, Record<string, never>, IUserSignUp, Record<string, never>>,
+ res: Response): Promise<void> {
+ try {
+ const { name } = req.body;
+
+ const user = await User.findOne({ name }).exec();
+ Iif (user) {
+ res.status(status.CONFLICT).json({ message: `User already exists: ${name}` });
+ return;
+ }
+
+ const newUser = await createUser(req.body);
+ Iif (!newUser) {
+ res.status(status.INTERNAL_SERVER_ERROR).json({ message: 'User registered Failed' });
+ return;
+ }
+
+ res.status(status.OK).json(newUser.toJSON());
+ } catch (error) {
+ handleErrorResponse(error, res);
+ }
+
+}
+
+export async function signIn(
+ req: Request<Record<string, never>, Record<string, never>, IUserSignIn, Record<string, never>>,
+ res: Response): Promise<void> {
+ try {
+ const { email, password } = req.body;
+ const user = await User.findOne({ email }).exec();
+ Iif (!user) {
+ res.status(status.NOT_FOUND).json({ message: `User Not found. User email: ${email}` });
+ return;
+ }
+
+ const isPasswordValid = bcrypt.compareSync(password, user.password);
+ Iif (!isPasswordValid) {
+ res.status(status.UNAUTHORIZED).json({
+ message: "Invalid Password!",
+ });
+ return;
+ }
+
+ const token = await user.generateToken();
+ Iif (!token) {
+ res.status(status.INTERNAL_SERVER_ERROR).json({ message: 'Failed to generate token' });
+ return;
+ }
+
+ res.status(status.OK).json(user.toJSON());
+ } catch (error) {
+ handleErrorResponse(error, res);
+ }
+}
+
+export async function signOut(
+ req: Request<Record<string, never>, Record<string, never>, IUserSignOut, Record<string, never>>,
+ res: Response): Promise<void> {
+
+ try {
+ const { name } = req.body;
+ const user = await User.findOne({ name }).exec();
+ Iif (!user) {
+ res.status(status.NOT_FOUND).json({ message: `User Not found. User name: ${name}` });
+ return;
+ }
+
+ await user.deleteToken();
+
+ res.status(status.OK).json({ message: "You've been signed out!" });
+ } catch (error) {
+ handleErrorResponse(error, res);
+ }
+}
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| auth.controller.ts | +
+
+ |
+ 17.77% | +8/45 | +0% | +0/6 | +0% | +0/3 | +17.77% | +8/45 | +
| user.controller.ts | +
+
+ |
+ 20% | +11/55 | +0% | +0/11 | +0% | +0/7 | +22% | +11/50 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 | 1x + +1x +1x + +1x + +1x + +1x + + + + + + + + + +1x + + + + + + + + + + + + + + +1x + + + + + + + + + + + + + + + + + + + + + + + +1x + + + + + + + + + +1x + + + + + + + + + + + + + + + + +1x + + + + + + + + + + + + + + | import bcrypt from 'bcrypt';
+import { Request, Response } from 'express';
+import status from 'http-status';
+import User from '../models/user.model';
+import { IUser, IUserSignUp, IUserUpdate } from '@interfaces/user';
+import handleErrorResponse from '../utils/controller.helper';
+
+const saltOrRounds = 10;
+
+export async function createUser(user: IUserSignUp) {
+ try {
+ user.password = bcrypt.hashSync(user.password, saltOrRounds);
+ return await new User(user).save();
+ } catch (error) {
+ console.error(error);
+ return null;
+ }
+}
+
+export async function getUser(req: Request<{ userId: string }>, res: Response) {
+ try {
+ const {
+ userId
+ } = req.params;
+
+ const user = await User.findById(userId).exec();
+
+ if (user) res.status(status.OK).json(user.toJSON());
+ else res.status(status.NOT_FOUND).json({ message: 'Cannot find the user' });
+ } catch (error) {
+ handleErrorResponse(error, res);
+ }
+}
+
+export async function updateUser(
+ req: Request<{ userId: string }, Record<string, never>, IUserUpdate, Record<string, never>>,
+ res: Response) {
+ try {
+ const {
+ params: { userId },
+ } = req;
+
+ const payload = req.body;
+
+ Iif (!payload || !Object.keys(payload).length) {
+ res.status(status.BAD_REQUEST).json({ message: 'Request cannot be empty' });
+ return;
+ }
+
+ const user = await User.findByIdAndUpdate(userId, payload);
+
+ if (user) res.status(status.OK).json({ message: 'Update user successfully' });
+ else res.status(status.NOT_FOUND).json({ message: 'Failed to update the user, cannot find the user' });
+ } catch (error) {
+ handleErrorResponse(error, res);
+ }
+}
+
+export async function getAllUsers(req: Request, res: Response) {
+ try {
+ const userDocs = await User.find({});
+ const users = await userDocs.map(userDoc => userDoc.toJSON());
+ res.status(status.OK).json(users);
+ } catch (error) {
+ handleErrorResponse(error, res);
+ }
+}
+
+export async function changeRole(
+ req: Request<{ userId: string }, Record<string, never>, Pick<IUser, 'role'>, Record<string, never>>,
+ res: Response) {
+ try {
+ const { userId } = req.params;
+
+ const { role } = req.body;
+
+ const user = await User.findByIdAndUpdate(userId, { role });
+
+ if (user) res.status(status.OK).json({ message: 'Changed user role successfully' });
+ else res.status(status.NOT_FOUND).json({ message: 'Failed to update the user, cannot find the user' });
+ } catch (error) {
+ handleErrorResponse(error, res);
+ }
+}
+
+export async function deleteUser(req: Request<{ userId: string }>, res: Response) {
+ try {
+ const {
+ params: { userId },
+ } = req;
+
+ const user = await User.findByIdAndDelete(userId);
+
+ if (user) res.status(status.OK).json({ message: 'Successfully deleted the user' });
+ else res.status(status.NOT_FOUND).json({ message: 'Failed to delete the user, cannot find the user' });
+ } catch (error) {
+ handleErrorResponse(error, res);
+ }
+}
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| sum.js | +
+
+ |
+ 100% | +2/2 | +100% | +0/0 | +100% | +1/1 | +100% | +2/2 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 | 1x + +1x +1x + + + + + +1x + + + + + + + + + + + + + + + + + + + + + + + +1x +1x + + + +1x + + + + + + + + + + + + + +1x + | import Joi from 'joi';
+
+import { config as dotenvConfig } from 'dotenv';
+dotenvConfig({
+ path: ['.env.local', '.env']
+});
+
+
+// define validation for all the env vars
+const envVarsSchema = Joi.object({
+ NODE_ENV: Joi.string()
+ .allow('development', 'production', 'test', 'provision')
+ .default(process.env.NODE_ENV),
+ SERVER_PORT: Joi.number().default(process.env.SERVER_PORT),
+ // MONGOOSE_DEBUG: Joi.boolean().when('NODE_ENV', {
+ // is: Joi.string().equal('development'),
+ // then: Joi.boolean().default(true),
+ // otherwise: Joi.boolean().default(false),
+ // }),
+ JWT_SECRET: Joi.string().default(process.env.JWT_SECRET)
+ .required()
+ .description('JWT Secret required to sign'),
+ DB_CONN_STRING: Joi.string().default(process.env.DB_CONN_STRING),
+ DB_NAME: Joi.string().default(process.env.DB_NAME),
+ SMTP_HOST: Joi.string().default(process.env.SMTP_HOST),
+ SMTP_PORT: Joi.number().default(process.env.SMTP_PORT),
+ SMTP_USERNAME: Joi.string().default(process.env.SMTP_USERNAME),
+ SMTP_PASSWORD: Joi.string().default(process.env.SMTP_PASSWORD),
+ SMTP_SENDER: Joi.string().default(process.env.SMTP_SENDER)
+})
+ .unknown()
+ .required();
+
+const { error, value: envVars } = envVarsSchema.validate(process.env);
+if (error) {
+ //throw new Error(`Config validation error: ${error.message}`);
+}
+
+const config = {
+ env: envVars.NODE_ENV,
+ port: envVars.SERVER_PORT,
+ // mongooseDebug: envVars.MONGOOSE_DEBUG,
+ jwtSecret: envVars.JWT_SECRET,
+ dbConnString: envVars.DB_CONN_STRING,
+ dbName: envVars.DB_NAME,
+ smtpHost: envVars.SMTP_HOST,
+ smtpPort: envVars.SMTP_PORT,
+ smtpUsername: envVars.SMTP_USERNAME,
+ smtpPassword: envVars.SMTP_PASSWORD,
+ smtpSender: envVars.SMTP_SENDER
+};
+
+export default config;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| config.ts | +
+
+ |
+ 100% | +8/8 | +100% | +1/1 | +100% | +0/0 | +100% | +8/8 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| userRoles.ts | +
+
+ |
+ 100% | +1/1 | +100% | +0/0 | +100% | +0/0 | +100% | +1/1 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 | 4x + + + + + + + | export const UserRole = {
+ ADMIN: 'admin',
+ visitor: 'user',
+ GUEST: 'guest'
+} as const;
+
+export type TUserRole = typeof UserRole[keyof typeof UserRole];
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| rbac.middleware.ts | +
+
+ |
+ 89.65% | +26/29 | +70% | +7/10 | +100% | +2/2 | +95.45% | +21/22 | +
| validate.middleware .ts | +
+
+ |
+ 93.75% | +15/16 | +75% | +3/4 | +100% | +4/4 | +90.9% | +10/11 | +
| validators.ts | +
+
+ |
+ 100% | +10/10 | +100% | +0/0 | +100% | +0/0 | +100% | +10/10 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 | 1x + +1x +1x + +6x +6x +6x +6x + +6x +6x + +5x +5x +1x + +4x +4x + +4x +3x + +2x + +1x + +1x + +1x + + + + +1x + | import status from 'http-status';
+import { Request, Response, NextFunction } from "express";
+import User from '../models/user.model';
+import { UserRole } from '../constants/userRoles';
+
+const validateToken = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const headers = req.headers;
+ Iif (!headers) return res.status(status.BAD_REQUEST).json({ message: 'No headers found' });
+
+ const authHeader = req.headers.authorization;
+ if (!authHeader) return res.status(status.UNAUTHORIZED).json({ message: 'No authorization header found' });
+
+ const tokenParts = authHeader.split(' ');
+ if (tokenParts.length !== 2 || tokenParts[0] !== 'Bearer')
+ return res.status(status.UNAUTHORIZED).json({ message: 'Invalid authorization header format. Expected format: Bearer <token>' });
+
+ const token = tokenParts[1];
+ Iif (!token) return res.status(status.UNAUTHORIZED).json({ message: 'Token not found' });
+
+ const user = await User.findOne({ token });
+ if (!user) return res.status(status.NOT_FOUND).json({ message: 'Cannot find the user' });
+
+ if (user.role !== UserRole.ADMIN) return res.status(status.FORBIDDEN).json({ message: 'Access denied' });
+
+ next();
+ } catch (error) {
+ console.error(error);
+
+ if (error instanceof Error) return res.status(status.INTERNAL_SERVER_ERROR).json({ message: error.message });
+ else Ereturn res.status(status.INTERNAL_SERVER_ERROR).json({ message: 'Unknown error occurred' });
+ }
+}
+
+export default validateToken;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 | 1x + +1x + +3x + +3x +3x +1x + +2x + +2x +1x + + + + +1x + | import status from 'http-status';
+import { Request, Response, NextFunction } from "express";
+import { Schema, ValidationError } from "joi";
+
+const validateRequest = (schema: Schema) => async (req: Request, res: Response, next: NextFunction) => {
+
+ try {
+ await schema.validateAsync(req.body);
+ next();
+ } catch (error) {
+ console.error(error);
+
+ if (error instanceof ValidationError) res.status(status.BAD_REQUEST).json({ message: error.details.map(detail => detail.message) });
+ else if (error instanceof Error) res.status(status.INTERNAL_SERVER_ERROR).json({ message: error.message });
+ else Eres.status(status.INTERNAL_SERVER_ERROR).json({ message: 'Unknown error occurred' });
+ }
+}
+
+export default validateRequest;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 | 1x +1x + +1x +1x + +1x + + + + + + + +1x + + + + +1x + + + + +1x + + + + + + + +1x + + + +1x + + + | import { UserRole } from "../constants/userRoles";
+import Joi, { EmailOptions } from "joi";
+
+const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+={}[\]:;<>,.?/~\\|-]).{8,16}$/;
+const emailOptions: EmailOptions = Object.freeze({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } });
+
+export const signUpSchema = Joi.object({
+ name: Joi.string().min(3).max(30).required(),
+ email: Joi.string().email(emailOptions).required(),
+ // mobileNumber: Joi.string().regex(/^[1-9][0-9]{9}$/),
+ password: Joi.string().pattern(new RegExp(passwordRegex)).required(),
+ repeatPassword: Joi.ref('password')
+});
+
+export const signInSchema = Joi.object({
+ email: Joi.string().email(emailOptions).required(),
+ password: Joi.string().pattern(new RegExp(passwordRegex)).required()
+});
+
+export const signOutSchema = Joi.object({
+ name: Joi.string().min(3).max(30).required(),
+ token: Joi.string().required()
+});
+
+export const updateUserSchema = Joi.object({
+ name: Joi.string().min(3).max(30),
+ email: Joi.string().email(emailOptions),
+ password: Joi.string().pattern(new RegExp(passwordRegex)),
+ // role: Joi.string().valid(...Object.values(UserRole)),
+ // token: Joi.string(),
+});
+
+export const changeRoleSchema = Joi.object({
+ role: Joi.string().valid(...Object.values(UserRole)).required()
+}).unknown(false);
+
+export const sendOtpSchema = Joi.object({
+ email: Joi.string().email(emailOptions),
+}).unknown(false);
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| user.model.ts | +
+
+ |
+ 37.03% | +10/27 | +100% | +0/0 | +0% | +0/3 | +40% | +10/25 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 | 1x + +1x +1x +1x + +1x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1x + + + + + + + + + + + + + + + +1x + + + + + + + + +1x + + + + + + + + + +1x + +1x + | import { UserRole } from "../constants/userRoles";
+import { IUserDoc, IUserMethods, IUserModel } from "@interfaces/user";
+import { Schema, model } from "mongoose";
+import jwt from 'jsonwebtoken';
+import config from "../config/config";
+
+const uerSchema = new Schema<IUserDoc, IUserModel, IUserMethods>({
+ name: {
+ type: String,
+ required: true,
+ trim: true,
+ },
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ trim: true,
+ match: [
+ // eslint-disable-next-line no-useless-escape
+ /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
+ 'Please enter a valid email',
+ ],
+ },
+ password: {
+ type: String,
+ required: true,
+ },
+ role: {
+ type: String,
+ enum: {
+ values: Object.values(UserRole),
+ message: '{ROLE} does not exist',
+ },
+ default: UserRole.visitor
+ },
+ token: {
+ type: String,
+ default: ''
+ },
+ createdAt: {
+ type: Date,
+ default: Date.now
+ }
+});
+
+uerSchema.methods.generateToken = async function (): Promise<string> {
+ try {
+ /**
+ * the data type of this is:
+ * Document<unknown, {}, FlatRecord<IUserDoc>> & Omit<FlatRecord<IUserDoc> & Required<{ _id: unknown; }>, keyof IUserMethods> & IUserMethods
+ */
+ const token = jwt.sign({ name: this.name }, config.jwtSecret, { expiresIn: 86400 });
+ this.token = token;
+ await this.save();
+ return token;
+ } catch (error) {
+ console.error(error);
+ return '';
+ }
+}
+
+uerSchema.methods.deleteToken = async function (): Promise<void> {
+ try {
+ this.token = '';
+ await this.save();
+ } catch (error) {
+ console.error(error);
+ }
+}
+
+uerSchema.methods.toJSON = function () {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { password, ...userObject } = this.toObject();
+ return userObject;
+ } catch (error) {
+ return null;
+ }
+}
+
+const User = model<IUserDoc, IUserModel>('users', uerSchema, 'users');
+
+export default User;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 | +1x + +1x | function sum(a, b) {
+ return a + b;
+}
+module.exports = sum; |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| constants | +
+
+ |
+ 100% | +1/1 | +100% | +0/0 | +100% | +0/0 | +100% | +1/1 | +
| middleware | +
+
+ |
+ 100% | +10/10 | +100% | +0/0 | +100% | +0/0 | +100% | +10/10 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| validators.ts | +
+
+ |
+ 100% | +10/10 | +100% | +0/0 | +100% | +0/0 | +100% | +10/10 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 | 1x + +1x +1x + +6x +6x +6x +6x + +6x +6x + +5x +5x +1x + +4x +4x + +4x +3x + +2x + +1x + +1x + +1x + + + + +1x + | import status from 'http-status';
+import { Request, Response, NextFunction } from "express";
+import User from '../models/user.model';
+import { UserRole } from '../constants/userRoles';
+
+const validateToken = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const headers = req.headers;
+ Iif (!headers) return res.status(status.BAD_REQUEST).json({ message: 'No headers found' });
+
+ const authHeader = req.headers.authorization;
+ if (!authHeader) return res.status(status.UNAUTHORIZED).json({ message: 'No authorization header found' });
+
+ const tokenParts = authHeader.split(' ');
+ if (tokenParts.length !== 2 || tokenParts[0] !== 'Bearer')
+ return res.status(status.UNAUTHORIZED).json({ message: 'Invalid authorization header format. Expected format: Bearer <token>' });
+
+ const token = tokenParts[1];
+ Iif (!token) return res.status(status.UNAUTHORIZED).json({ message: 'Token not found' });
+
+ const user = await User.findOne({ token });
+ if (!user) return res.status(status.NOT_FOUND).json({ message: 'Cannot find the user' });
+
+ if (user.role !== UserRole.ADMIN) return res.status(status.FORBIDDEN).json({ message: 'Access denied' });
+
+ next();
+ } catch (error) {
+ console.error(error);
+
+ if (error instanceof Error) return res.status(status.INTERNAL_SERVER_ERROR).json({ message: error.message });
+ else Ereturn res.status(status.INTERNAL_SERVER_ERROR).json({ message: 'Unknown error occurred' });
+ }
+}
+
+export default validateToken;
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 | 1x +1x + +1x +1x + +1x + + + + + + + +1x + + + + +1x + + + + +1x + + + + + + + +1x + + + +1x + + + | import { UserRole } from "../constants/userRoles";
+import Joi, { EmailOptions } from "joi";
+
+const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+={}[\]:;<>,.?/~\\|-]).{8,16}$/;
+const emailOptions: EmailOptions = Object.freeze({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } });
+
+export const signUpSchema = Joi.object({
+ name: Joi.string().min(3).max(30).required(),
+ email: Joi.string().email(emailOptions).required(),
+ // mobileNumber: Joi.string().regex(/^[1-9][0-9]{9}$/),
+ password: Joi.string().pattern(new RegExp(passwordRegex)).required(),
+ repeatPassword: Joi.ref('password')
+});
+
+export const signInSchema = Joi.object({
+ email: Joi.string().email(emailOptions).required(),
+ password: Joi.string().pattern(new RegExp(passwordRegex)).required()
+});
+
+export const signOutSchema = Joi.object({
+ name: Joi.string().min(3).max(30).required(),
+ token: Joi.string().required()
+});
+
+export const updateUserSchema = Joi.object({
+ name: Joi.string().min(3).max(30),
+ email: Joi.string().email(emailOptions),
+ password: Joi.string().pattern(new RegExp(passwordRegex)),
+ // role: Joi.string().valid(...Object.values(UserRole)),
+ // token: Joi.string(),
+});
+
+export const changeRoleSchema = Joi.object({
+ role: Joi.string().valid(...Object.values(UserRole)).required()
+}).unknown(false);
+
+export const sendOtpSchema = Joi.object({
+ email: Joi.string().email(emailOptions),
+}).unknown(false);
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| user.model.ts | +
+
+ |
+ 37.03% | +10/27 | +100% | +0/0 | +0% | +0/3 | +40% | +10/25 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 | 1x + +1x +1x +1x + +1x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1x + + + + + + + + + + + + + + + +1x + + + + + + + + +1x + + + + + + + + + +1x + +1x + | import { UserRole } from "../constants/userRoles";
+import { IUserDoc, IUserMethods, IUserModel } from "@interfaces/user";
+import { Schema, model } from "mongoose";
+import jwt from 'jsonwebtoken';
+import config from "../config/config";
+
+const uerSchema = new Schema<IUserDoc, IUserModel, IUserMethods>({
+ name: {
+ type: String,
+ required: true,
+ trim: true,
+ },
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ trim: true,
+ match: [
+ // eslint-disable-next-line no-useless-escape
+ /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
+ 'Please enter a valid email',
+ ],
+ },
+ password: {
+ type: String,
+ required: true,
+ },
+ role: {
+ type: String,
+ enum: {
+ values: Object.values(UserRole),
+ message: '{ROLE} does not exist',
+ },
+ default: UserRole.visitor
+ },
+ token: {
+ type: String,
+ default: ''
+ },
+ createdAt: {
+ type: Date,
+ default: Date.now
+ }
+});
+
+uerSchema.methods.generateToken = async function (): Promise<string> {
+ try {
+ /**
+ * the data type of this is:
+ * Document<unknown, {}, FlatRecord<IUserDoc>> & Omit<FlatRecord<IUserDoc> & Required<{ _id: unknown; }>, keyof IUserMethods> & IUserMethods
+ */
+ const token = jwt.sign({ name: this.name }, config.jwtSecret, { expiresIn: 86400 });
+ this.token = token;
+ await this.save();
+ return token;
+ } catch (error) {
+ console.error(error);
+ return '';
+ }
+}
+
+uerSchema.methods.deleteToken = async function (): Promise<void> {
+ try {
+ this.token = '';
+ await this.save();
+ } catch (error) {
+ console.error(error);
+ }
+}
+
+uerSchema.methods.toJSON = function () {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { password, ...userObject } = this.toObject();
+ return userObject;
+ } catch (error) {
+ return null;
+ }
+}
+
+const User = model<IUserDoc, IUserModel>('users', uerSchema, 'users');
+
+export default User;
+ |