diff --git a/src/config/app.config.ts b/src/config/app.config.ts index b2b4c9f7..a78c1c1c 100644 --- a/src/config/app.config.ts +++ b/src/config/app.config.ts @@ -1,7 +1,7 @@ -import config from "."; - -const APP_CONFIG = Object.freeze({ - USE_HTTPS: config.NODE_ENV !== "development" ? true : false, -}); - -export default APP_CONFIG; +import config from "."; + +const APP_CONFIG = Object.freeze({ + USE_HTTPS: config.NODE_ENV !== "development" ? true : false, +}); + +export default APP_CONFIG; diff --git a/src/config/cloudinary.ts b/src/config/cloudinary.ts index 4e8a9b1e..4498a298 100644 --- a/src/config/cloudinary.ts +++ b/src/config/cloudinary.ts @@ -1,16 +1,16 @@ -import * as cloudinary from "cloudinary"; -import { ConfigOptions } from "cloudinary"; - -const cloudinaryConfig = ( - cloudName: string, - apiKey: string, - apiSecret: string, -): ConfigOptions => { - return cloudinary.v2.config({ - cloud_name: cloudName, - api_key: apiKey, - api_secret: apiSecret, - }); -}; - -export default cloudinaryConfig; +import * as cloudinary from "cloudinary"; +import { ConfigOptions } from "cloudinary"; + +const cloudinaryConfig = ( + cloudName: string, + apiKey: string, + apiSecret: string, +): ConfigOptions => { + return cloudinary.v2.config({ + cloud_name: cloudName, + api_key: apiKey, + api_secret: apiSecret, + }); +}; + +export default cloudinaryConfig; diff --git a/src/config/google.passport.config.ts b/src/config/google.passport.config.ts index 694a68d8..3033809b 100644 --- a/src/config/google.passport.config.ts +++ b/src/config/google.passport.config.ts @@ -1,46 +1,46 @@ -import passport from "passport"; -import { - Strategy as GoogleStrategy, - Profile, - VerifyCallback, -} from "passport-google-oauth2"; -import config from "."; - -import { OAuth2Client } from "google-auth-library"; -const client = new OAuth2Client(); - -passport.use( - new GoogleStrategy( - { - clientID: config.GOOGLE_CLIENT_ID, - clientSecret: config.GOOGLE_CLIENT_SECRET, - callbackURL: config.GOOGLE_AUTH_CALLBACK_URL, - }, - async ( - _accessToken: string, - _refreshToken: string, - profile: Profile, - done: VerifyCallback, - ) => { - try { - return done(null, profile); - } catch (error) { - return done(error); - } - }, - ), -); - -export default passport; - -export async function verifyToken(idToken: string) { - const ticket = await client.verifyIdToken({ - idToken, - audience: process.env.GOOGLE_CLIENT_ID, - }); - const payload = ticket.getPayload(); - if (!payload) { - throw new Error("Unable to verify token"); - } - return payload; -} +import passport from "passport"; +import { + Strategy as GoogleStrategy, + Profile, + VerifyCallback, +} from "passport-google-oauth2"; +import config from "."; + +import { OAuth2Client } from "google-auth-library"; +const client = new OAuth2Client(); + +passport.use( + new GoogleStrategy( + { + clientID: config.GOOGLE_CLIENT_ID, + clientSecret: config.GOOGLE_CLIENT_SECRET, + callbackURL: config.GOOGLE_AUTH_CALLBACK_URL, + }, + async ( + _accessToken: string, + _refreshToken: string, + profile: Profile, + done: VerifyCallback, + ) => { + try { + return done(null, profile); + } catch (error) { + return done(error); + } + }, + ), +); + +export default passport; + +export async function verifyToken(idToken: string) { + const ticket = await client.verifyIdToken({ + idToken, + audience: process.env.GOOGLE_CLIENT_ID, + }); + const payload = ticket.getPayload(); + if (!payload) { + throw new Error("Unable to verify token"); + } + return payload; +} diff --git a/src/config/multer.ts b/src/config/multer.ts index a4b2beb4..9102a860 100644 --- a/src/config/multer.ts +++ b/src/config/multer.ts @@ -1,47 +1,47 @@ -import multer from "multer"; -import { v2 as cloudinary } from "cloudinary"; -import { CloudinaryStorage } from "multer-storage-cloudinary"; -import cloudinaryConfig from "./cloudinary"; -import * as dotenv from "dotenv"; -dotenv.config(); - -interface CustomParams { - folder: string; - allowedFormats: string[]; -} - -const cloudinaryConfigOptions = cloudinaryConfig( - process.env["CLOUDINARY_CLOUD_NAME"] as string, - process.env["CLOUDINARY_API_KEY"] as string, - process.env["CLOUDINARY_API_SECRET"] as string, -); - -cloudinary.config(cloudinaryConfigOptions); - -const storage = new CloudinaryStorage({ - cloudinary, - params: { - folder: "images", - allowedFormats: ["jpg", "png", "jpeg"], - } as CustomParams, -}); - -const FILE_SIZE = 1024 * 1024 * 2; // 2MB - -export const multerConfig = multer({ - storage, - limits: { - fileSize: FILE_SIZE, - }, - fileFilter: (_req, file, cb) => { - if (!file.mimetype.startsWith("image/")) { - return cb(new Error("Only images are allowed")); - } - if (file.size > FILE_SIZE) { - return cb(new Error("Image should not be more than 4MB")); - } - cb(null, true); - }, -}); - -export { cloudinary }; +import multer from "multer"; +import { v2 as cloudinary } from "cloudinary"; +import { CloudinaryStorage } from "multer-storage-cloudinary"; +import cloudinaryConfig from "./cloudinary"; +import * as dotenv from "dotenv"; +dotenv.config(); + +interface CustomParams { + folder: string; + allowedFormats: string[]; +} + +const cloudinaryConfigOptions = cloudinaryConfig( + process.env["CLOUDINARY_CLOUD_NAME"] as string, + process.env["CLOUDINARY_API_KEY"] as string, + process.env["CLOUDINARY_API_SECRET"] as string, +); + +cloudinary.config(cloudinaryConfigOptions); + +const storage = new CloudinaryStorage({ + cloudinary, + params: { + folder: "images", + allowedFormats: ["jpg", "png", "jpeg"], + } as CustomParams, +}); + +const FILE_SIZE = 1024 * 1024 * 2; // 2MB + +export const multerConfig = multer({ + storage, + limits: { + fileSize: FILE_SIZE, + }, + fileFilter: (_req, file, cb) => { + if (!file.mimetype.startsWith("image/")) { + return cb(new Error("Only images are allowed")); + } + if (file.size > FILE_SIZE) { + return cb(new Error("Image should not be more than 4MB")); + } + cb(null, true); + }, +}); + +export { cloudinary }; diff --git a/src/controllers/AdminController.ts b/src/controllers/AdminController.ts index 6baad6ac..57b889ab 100644 --- a/src/controllers/AdminController.ts +++ b/src/controllers/AdminController.ts @@ -1,682 +1,682 @@ -import { Request, Response } from "express"; -import { - AdminOrganisationService, - AdminUserService, - AdminLogService, -} from "../services"; -import { HttpError } from "../middleware"; -import { check, param, validationResult } from "express-validator"; -import { UserRole } from "../enums/userRoles"; - -/** - * @swagger - * tags: - * name: Admin - * description: Admin Related Routes - */ - -/** - * @swagger - * /api/v1/admin/organisation/{id}: - * patch: - * summary: Admin-Update an existing organisation - * tags: [Admin] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * format: uuid - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * email: - * type: string - * slug: - * type: string - * type: - * type: string - * industry: - * type: string - * state: - * type: string - * country: - * type: string - * address: - * type: string - * responses: - * 200: - * description: Organisation Updated Successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * data: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * email: - * type: string - * slug: - * type: string - * type: - * type: string - * industry: - * type: string - * state: - * type: string - * country: - * type: string - * address: - * type: string - * created_at: - * type: string - * format: date-time - * updated_at: - * type: string - * format: date-time - * status_code: - * type: integer - * example: 200 - * 400: - * description: Bad Request - * 500: - * description: Internal Server Error - */ - -class AdminOrganisationController { - private adminService: AdminOrganisationService; - - constructor() { - this.adminService = new AdminOrganisationService(); - } - - async updateOrg(req: Request, res: Response): Promise { - try { - const org = await this.adminService.update(req); - res.status(200).json({ - success: true, - message: "Organisation Updated Successfully", - status_code: 200, - }); - } catch (error) { - if (error instanceof HttpError) { - res.status(error.status_code).json({ - success: false, - message: error.message, - status_code: error.status_code, - }); - } else { - res - .status(500) - .json({ message: error.message || "Internal Server Error" }); - } - } - } - - async setUserRole(req: Request, res: Response): Promise { - try { - await param("user_id") - .isUUID() - .withMessage("Valid user ID must be provided.") - .run(req); - await check("role") - .isIn(Object.values(UserRole)) - .withMessage("Valid role must be provided.") - .run(req); - - const errors = validationResult(req); - - if (!errors.isEmpty()) { - res.status(422).json({ - status: "unsuccessful", - status_code: 422, - message: errors.array()[0].msg, - }); - return; - } - const user = await this.adminService.setUserRole(req); - res.status(200).json({ - success: true, - message: "User role updated successfully", - data: { - id: user.id, - username: user.name, - role: user.role, - }, - }); - } catch (error) { - res - .status(error.status_code || 500) - .json({ message: error.message || "Internal Server Error" }); - } - } - - /** - * @swagger - * /api/v1/admin/organizations/{org_id}/delete: - * delete: - * summary: Admin-Delete an existing organization - * tags: [Admin] - * parameters: - * - in: path - * name: org_id - * required: true - * description: The ID of the organization to delete - * schema: - * type: string - * responses: - * 200: - * description: Organization deleted successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * data: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * description: - * type: string - * createdAt: - * type: string - * format: date-time - * updatedAt: - * type: string - * format: date-time - * 400: - * description: Valid organization ID must be provided - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * status_code: - * type: integer - * example: 400 - * message: - * type: string - * 404: - * description: Organization not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * status_code: - * type: integer - * example: 404 - * message: - * type: string - * 500: - * description: Failed to delete organization. Please try again later. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * status_code: - * type: integer - * example: 500 - * message: - * type: string - */ - - // Delete organization - async deleteOrganization(req: Request, res: Response) { - const { org_id } = req.params; - - if (!org_id) { - return res.status(400).json({ - status: "unsuccessful", - status_code: 400, - message: "Valid organization ID must be provided.", - }); - } - - try { - await this.adminService.deleteOrganization(org_id); - res.status(200).json({ - status: "success", - status_code: 200, - message: "Organization deleted successfully.", - }); - } catch (error) { - res.status(500).json({ - status: "unsuccessful", - status_code: 500, - message: "Failed to delete organization. Please try again later.", - }); - } - } -} - -class AdminUserController { - private adminUserService: AdminUserService; - - constructor() { - this.adminUserService = new AdminUserService(); - } - - /** - * @swagger - * /api/v1/admin/users/{id}: - * patch: - * summary: Admin-Update an existing user - * tags: [Admin] - * parameters: - * - in: path - * name: id - * required: true - * description: The ID of the user to update - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * email: - * type: string - * role: - * type: string - * isverified: - * type: boolean - * responses: - * 200: - * description: User Updated Successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * data: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * email: - * type: string - * role: - * type: string - * isverified: - * type: boolean - * createdAt: - * type: string - * format: date-time - * updatedAt: - * type: string - * format: date-time - * status_code: - * type: integer - * 400: - * description: Bad Request - * 404: - * description: User Not Found - * 500: - * description: Internal Server Error - */ - - //Update Single User - async updateUser(req: Request, res: Response): Promise { - try { - const user = await this.adminUserService.updateUser(req); - res.status(200).json({ - success: true, - message: "User Updated Successfully", - data: { - id: user.id, - name: user.name, - email: user.email, - role: user.role, - isverified: user.isverified, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - }, - status_code: 200, - }); - } catch (error) { - if (error instanceof HttpError) { - res.status(error.status_code).json({ message: error.message }); - } else { - res - .status(500) - .json({ message: error.message || "Internal Server Error" }); - } - } - } - - /** - * @swagger - * /api/v1/admin/users: - * get: - * summary: Admin-List users with pagination - * tags: [Admin] - * parameters: - * - in: query - * name: page - * required: false - * description: Page number for pagination - * schema: - * type: integer - * default: 1 - * - in: query - * name: limit - * required: false - * description: Number of users per page - * schema: - * type: integer - * default: 5 - * responses: - * 200: - * description: Users retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * users: - * type: array - * items: - * type: object - * properties: - * name: - * type: string - * email: - * type: string - * createdAt: - * type: string - * format: date-time - * updatedAt: - * type: string - * format: date-time - * pagination: - * type: object - * properties: - * totalUsers: - * type: integer - * totalPages: - * type: integer - * currentPage: - * type: integer - * status_code: - * type: integer - * 400: - * description: Bad Request - * 500: - * description: Internal Server Error - */ - - async listUsers(req: Request, res: Response): Promise { - try { - const page = parseInt(req.query.page as string) || 1; - const limit = parseInt(req.query.limit as string) || 5; - - if (page <= 0 || limit <= 0) { - res.status(400).json({ - status: "unsuccessful", - status_code: 400, - message: - "Invalid pagination parameters. Page and limit must be positive integers.", - }); - return; - } - - const { users, totalUsers } = - await this.adminUserService.getPaginatedUsers(page, limit); - const pages = Math.ceil(totalUsers / limit); - - if (page > pages) { - res.status(400).json({ - status: "bad request", - message: `last page reached page: ${pages}`, - status_code: 400, - }); - return; - } - - res.json({ - success: true, - message: "Users retrieved successfully", - users: users.map((user) => ({ - name: user.name, - email: user.email, - role: user.role, - createdAt: user.createdAt, - })), - pagination: { - totalUsers, - totalPages: pages, - currentPage: page, - }, - status_code: 200, - }); - } catch (error) { - if (error instanceof HttpError) { - res.status(error.status_code).json({ message: error.message }); - } else { - res - .status(500) - .json({ message: error.message || "Internal Server Error" }); - } - } - } - - /** - * @swagger - * /api/v1/admin/users/{id}: - * get: - * summary: Superadmin - Get a single user - * tags: [Admin] - * parameters: - * - in: path - * name: id - * required: true - * description: The ID of the user data to retrieve - * schema: - * type: string - * responses: - * 200: - * description: User retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * data: - * type: object - * properties: - * user_id: - * type: string - * first_name: - * type: string - * last_name: - * type: string - * email: - * type: string - * phone: - * type: string - * profile_picture: - * type: string - * role: - * type: string - * status_code: - * type: integer - * example: 200 - * 404: - * description: User not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * message: - * type: string - * example: User not found - * status_code: - * type: integer - * example: 404 - * 500: - * description: Internal Server Error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: error - * message: - * type: string - * example: Internal Server Error - * status_code: - * type: integer - * example: 500 - */ - - async getUserBySuperadmin(req: Request, res: Response): Promise { - const userId = req.params.id; - try { - const user = await this.adminUserService.getSingleUser(userId); - if (!user) { - return { - status: "unsuccessful", - message: "User not found", - status_code: 404, - }; - } - return res.status(200).json({ - status: "success", - data: { - user_id: userId, - first_name: user.profile.first_name, - last_name: user.profile.last_name, - email: user.email, - phone: user.profile.phone_number, - profile_picture: user.profile.avatarUrl, - role: user.role, - }, - status_code: 200, - }); - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - } -} - -// Get activities log -class AdminLogController { - private adminLogService: AdminLogService; - - constructor() { - this.adminLogService = new AdminLogService(); - } - - async getLogs(req: Request, res: Response): Promise { - try { - await check("page") - .optional() - .isInt({ min: 1 }) - .withMessage("Page must be a positive integer.") - .run(req); - await check("limit") - .optional() - .isInt({ min: 1 }) - .withMessage("Limit must be a positive integer.") - .run(req); - await check("sort") - .optional() - .isIn(["asc", "desc"]) - .withMessage('Sort must be either "asc" or "desc".') - .run(req); - await check("offset") - .optional() - .isInt({ min: 0 }) - .withMessage("Offset must be a non-negative integer.") - .run(req); - - const errors = validationResult(req); - if (!errors.isEmpty()) { - res.status(422).json({ - status: "unsuccessful", - status_code: 422, - message: errors.array()[0].msg, - }); - return; - } - - const data = await this.adminLogService.getPaginatedLogs(req); - res.status(200).json({ - status: "success", - status_code: 200, - data, - }); - } catch (error) { - if (error instanceof HttpError) { - res.status(error.status_code).json({ message: error.message }); - } else { - res - .status(500) - .json({ message: error.message || "Internal Server Error" }); - } - } - } -} - -export default { - AdminOrganisationController, - AdminUserController, - AdminLogController, -}; +import { Request, Response } from "express"; +import { + AdminOrganisationService, + AdminUserService, + AdminLogService, +} from "../services"; +import { HttpError } from "../middleware"; +import { check, param, validationResult } from "express-validator"; +import { UserRole } from "../enums/userRoles"; + +/** + * @swagger + * tags: + * name: Admin + * description: Admin Related Routes + */ + +/** + * @swagger + * /api/v1/admin/organisation/{id}: + * patch: + * summary: Admin-Update an existing organisation + * tags: [Admin] + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: string + * format: uuid + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * email: + * type: string + * slug: + * type: string + * type: + * type: string + * industry: + * type: string + * state: + * type: string + * country: + * type: string + * address: + * type: string + * responses: + * 200: + * description: Organisation Updated Successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * email: + * type: string + * slug: + * type: string + * type: + * type: string + * industry: + * type: string + * state: + * type: string + * country: + * type: string + * address: + * type: string + * created_at: + * type: string + * format: date-time + * updated_at: + * type: string + * format: date-time + * status_code: + * type: integer + * example: 200 + * 400: + * description: Bad Request + * 500: + * description: Internal Server Error + */ + +class AdminOrganisationController { + private adminService: AdminOrganisationService; + + constructor() { + this.adminService = new AdminOrganisationService(); + } + + async updateOrg(req: Request, res: Response): Promise { + try { + const org = await this.adminService.update(req); + res.status(200).json({ + success: true, + message: "Organisation Updated Successfully", + status_code: 200, + }); + } catch (error) { + if (error instanceof HttpError) { + res.status(error.status_code).json({ + success: false, + message: error.message, + status_code: error.status_code, + }); + } else { + res + .status(500) + .json({ message: error.message || "Internal Server Error" }); + } + } + } + + async setUserRole(req: Request, res: Response): Promise { + try { + await param("user_id") + .isUUID() + .withMessage("Valid user ID must be provided.") + .run(req); + await check("role") + .isIn(Object.values(UserRole)) + .withMessage("Valid role must be provided.") + .run(req); + + const errors = validationResult(req); + + if (!errors.isEmpty()) { + res.status(422).json({ + status: "unsuccessful", + status_code: 422, + message: errors.array()[0].msg, + }); + return; + } + const user = await this.adminService.setUserRole(req); + res.status(200).json({ + success: true, + message: "User role updated successfully", + data: { + id: user.id, + username: user.name, + role: user.role, + }, + }); + } catch (error) { + res + .status(error.status_code || 500) + .json({ message: error.message || "Internal Server Error" }); + } + } + + /** + * @swagger + * /api/v1/admin/organizations/{org_id}/delete: + * delete: + * summary: Admin-Delete an existing organization + * tags: [Admin] + * parameters: + * - in: path + * name: org_id + * required: true + * description: The ID of the organization to delete + * schema: + * type: string + * responses: + * 200: + * description: Organization deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * data: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * description: + * type: string + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * 400: + * description: Valid organization ID must be provided + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * status_code: + * type: integer + * example: 400 + * message: + * type: string + * 404: + * description: Organization not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * status_code: + * type: integer + * example: 404 + * message: + * type: string + * 500: + * description: Failed to delete organization. Please try again later. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * status_code: + * type: integer + * example: 500 + * message: + * type: string + */ + + // Delete organization + async deleteOrganization(req: Request, res: Response) { + const { org_id } = req.params; + + if (!org_id) { + return res.status(400).json({ + status: "unsuccessful", + status_code: 400, + message: "Valid organization ID must be provided.", + }); + } + + try { + await this.adminService.deleteOrganization(org_id); + res.status(200).json({ + status: "success", + status_code: 200, + message: "Organization deleted successfully.", + }); + } catch (error) { + res.status(500).json({ + status: "unsuccessful", + status_code: 500, + message: "Failed to delete organization. Please try again later.", + }); + } + } +} + +class AdminUserController { + private adminUserService: AdminUserService; + + constructor() { + this.adminUserService = new AdminUserService(); + } + + /** + * @swagger + * /api/v1/admin/users/{id}: + * patch: + * summary: Admin-Update an existing user + * tags: [Admin] + * parameters: + * - in: path + * name: id + * required: true + * description: The ID of the user to update + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * email: + * type: string + * role: + * type: string + * isverified: + * type: boolean + * responses: + * 200: + * description: User Updated Successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * email: + * type: string + * role: + * type: string + * isverified: + * type: boolean + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * status_code: + * type: integer + * 400: + * description: Bad Request + * 404: + * description: User Not Found + * 500: + * description: Internal Server Error + */ + + //Update Single User + async updateUser(req: Request, res: Response): Promise { + try { + const user = await this.adminUserService.updateUser(req); + res.status(200).json({ + success: true, + message: "User Updated Successfully", + data: { + id: user.id, + name: user.name, + email: user.email, + role: user.role, + isverified: user.isverified, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + status_code: 200, + }); + } catch (error) { + if (error instanceof HttpError) { + res.status(error.status_code).json({ message: error.message }); + } else { + res + .status(500) + .json({ message: error.message || "Internal Server Error" }); + } + } + } + + /** + * @swagger + * /api/v1/admin/users: + * get: + * summary: Admin-List users with pagination + * tags: [Admin] + * parameters: + * - in: query + * name: page + * required: false + * description: Page number for pagination + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * required: false + * description: Number of users per page + * schema: + * type: integer + * default: 5 + * responses: + * 200: + * description: Users retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * users: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * email: + * type: string + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * pagination: + * type: object + * properties: + * totalUsers: + * type: integer + * totalPages: + * type: integer + * currentPage: + * type: integer + * status_code: + * type: integer + * 400: + * description: Bad Request + * 500: + * description: Internal Server Error + */ + + async listUsers(req: Request, res: Response): Promise { + try { + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 5; + + if (page <= 0 || limit <= 0) { + res.status(400).json({ + status: "unsuccessful", + status_code: 400, + message: + "Invalid pagination parameters. Page and limit must be positive integers.", + }); + return; + } + + const { users, totalUsers } = + await this.adminUserService.getPaginatedUsers(page, limit); + const pages = Math.ceil(totalUsers / limit); + + if (page > pages) { + res.status(400).json({ + status: "bad request", + message: `last page reached page: ${pages}`, + status_code: 400, + }); + return; + } + + res.json({ + success: true, + message: "Users retrieved successfully", + users: users.map((user) => ({ + name: user.name, + email: user.email, + role: user.role, + createdAt: user.createdAt, + })), + pagination: { + totalUsers, + totalPages: pages, + currentPage: page, + }, + status_code: 200, + }); + } catch (error) { + if (error instanceof HttpError) { + res.status(error.status_code).json({ message: error.message }); + } else { + res + .status(500) + .json({ message: error.message || "Internal Server Error" }); + } + } + } + + /** + * @swagger + * /api/v1/admin/users/{id}: + * get: + * summary: Superadmin - Get a single user + * tags: [Admin] + * parameters: + * - in: path + * name: id + * required: true + * description: The ID of the user data to retrieve + * schema: + * type: string + * responses: + * 200: + * description: User retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * data: + * type: object + * properties: + * user_id: + * type: string + * first_name: + * type: string + * last_name: + * type: string + * email: + * type: string + * phone: + * type: string + * profile_picture: + * type: string + * role: + * type: string + * status_code: + * type: integer + * example: 200 + * 404: + * description: User not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * message: + * type: string + * example: User not found + * status_code: + * type: integer + * example: 404 + * 500: + * description: Internal Server Error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Internal Server Error + * status_code: + * type: integer + * example: 500 + */ + + async getUserBySuperadmin(req: Request, res: Response): Promise { + const userId = req.params.id; + try { + const user = await this.adminUserService.getSingleUser(userId); + if (!user) { + return { + status: "unsuccessful", + message: "User not found", + status_code: 404, + }; + } + return res.status(200).json({ + status: "success", + data: { + user_id: userId, + first_name: user.profile.first_name, + last_name: user.profile.last_name, + email: user.email, + phone: user.profile.phone_number, + profile_picture: user.profile.avatarUrl, + role: user.role, + }, + status_code: 200, + }); + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + } +} + +// Get activities log +class AdminLogController { + private adminLogService: AdminLogService; + + constructor() { + this.adminLogService = new AdminLogService(); + } + + async getLogs(req: Request, res: Response): Promise { + try { + await check("page") + .optional() + .isInt({ min: 1 }) + .withMessage("Page must be a positive integer.") + .run(req); + await check("limit") + .optional() + .isInt({ min: 1 }) + .withMessage("Limit must be a positive integer.") + .run(req); + await check("sort") + .optional() + .isIn(["asc", "desc"]) + .withMessage('Sort must be either "asc" or "desc".') + .run(req); + await check("offset") + .optional() + .isInt({ min: 0 }) + .withMessage("Offset must be a non-negative integer.") + .run(req); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + res.status(422).json({ + status: "unsuccessful", + status_code: 422, + message: errors.array()[0].msg, + }); + return; + } + + const data = await this.adminLogService.getPaginatedLogs(req); + res.status(200).json({ + status: "success", + status_code: 200, + data, + }); + } catch (error) { + if (error instanceof HttpError) { + res.status(error.status_code).json({ message: error.message }); + } else { + res + .status(500) + .json({ message: error.message || "Internal Server Error" }); + } + } + } +} + +export default { + AdminOrganisationController, + AdminUserController, + AdminLogController, +}; diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 3480ab95..04660751 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -1,641 +1,641 @@ -import { NextFunction, Request, Response } from "express"; -import jwt from "jsonwebtoken"; -import config from "../config"; -import { verifyToken } from "../config/google.passport.config"; -import { BadRequest, Unauthorized } from "../middleware"; -import { User } from "../models"; -import { AuthService } from "../services/auth.services"; -import { GoogleUserInfo } from "../services/google.auth.service"; -import RequestUtils from "../utils/request.utils"; - -const authService = new AuthService(); - -/** - * @swagger - * tags: - * name: Auth - * description: Authentication related routes - */ - -/** - * @swagger - * api/v1/auth/register: - * post: - * summary: Sign up a new user - * tags: [Auth] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * first_name: - * type: string - * last_name: - * type: string - * email: - * type: string - * password: - * type: string - - * responses: - * 201: - * description: The user was successfully created - * content: - * application/json: - * schema: - * type: object - * properties: - * mailSent: - * type: string - * newUser: - * type: object - * access_token: - * type: string - * 409: - * description: User already exists - * 500: - * description: Some server error - */ - -const signUp = async (req: Request, res: Response, next: NextFunction) => { - try { - const { message, user, access_token } = await authService.signUp(req.body); - res.status(201).json({ message, user, access_token }); - } catch (error) { - console.log(error); - next(error); - } -}; - -/** - * @swagger - * /api/v1/auth/verify-otp: - * post: - * summary: Verify the user's email using OTP - * tags: [Auth] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * otp: - * type: integer - * description: The OTP sent to the user's email - * token: - * type: string - * description: The token received during sign up - * responses: - * 200: - * description: OTP verified successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * description: Success message - * 400: - * description: Invalid OTP or verification token has expired - * 404: - * description: User not found - * 500: - * description: Some server error - */ - -const verifyOtp = async (req: Request, res: Response, next: NextFunction) => { - try { - const { otp, token } = req.body; - const { message } = await authService.verifyEmail(token as string, otp); - res.status(200).json({ message }); - } catch (error) { - next(error); - } -}; - -/** - * @swagger - * /api/v1/auth/login: - * post: - * summary: Log in a user - * tags: [Auth] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * email: - * type: string - * password: - * type: string - * responses: - * 200: - * description: Successful login - * content: - * application/json: - * schema: - * type: object - * properties: - * access_token: - * type: string - * user: - * type: object - * 401: - * description: Invalid credentials - * 500: - * description: Some server error - */ - -const login = async (req: Request, res: Response, next: NextFunction) => { - try { - const { access_token, user } = await authService.login(req.body); - res.status(200).json({ access_token, user }); - } catch (error) { - next(error); - } -}; - -/** - * @swagger - * /api/v1/auth/forgotPassword: - * post: - * summary: Request a password reset - * description: Allows a user to request a password reset link by providing their email address. - * tags: [Auth] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - email - * properties: - * email: - * type: string - * description: The email address of the user. - * example: user@example.com - * responses: - * 200: - * description: Successfully requested password reset. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * description: The status of the request. - * example: success - * status_code: - * type: integer - * description: The HTTP status code. - * example: 200 - * message: - * type: string - * description: The message indicating the result of the request. - * example: Password reset link sent to your email. - * 400: - * description: Bad request - * 500: - * description: Internal server error. - */ - -const forgotPassword = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const { email } = req.body; - const resetURL = `${req.protocol}://${req.get("host")}/${config["api-prefix"]}/auth/reset-password/`; - const { message } = await authService.forgotPassword(email, resetURL); - - res.status(200).json({ status: "sucess", status_code: 200, message }); - } catch (error) { - next(error); - } -}; - -/** - * @swagger - * /api/v1/auth/reset-password/:token: - * post: - * summary: Reset a user's password - * description: Allows a user to reset their password by providing a valid reset token and a new password. - * tags: [Auth] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - token - * - newPassword - * properties: - * token: - * type: string - * description: The password reset token. - * example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - * newPassword: - * type: string - * description: The new password for the user. - * example: new_secure_password - * responses: - * 200: - * description: Successfully reset password. - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * description: The message indicating the result of the password reset. - * example: Password has been reset successfully. - * 400: - * description: Bad request. - * 500: - * description: Internal server error. - */ - -const resetPassword = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const { token } = req.params; - const { newPassword } = req.body; - const { message } = await authService.resetPassword(token, newPassword); - res.status(200).json({ status: "success", status_code: 200, message }); - } catch (error) { - next(error); - } -}; -/** - * @swagger - * /api/v1/auth/change-password: - * patch: - * summary: Change user password - * tags: [Auth] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * oldPassword: - * type: string - * description: Current password of the user - * newPassword: - * type: string - * description: New password to set for the user - * confirmPassword: - * type: string - * description: Confirmation of the new password - * responses: - * 200: - * description: Password changed successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Password changed successfully." - * 400: - * description: Bad request, such as mismatched passwords or invalid input - * 401: - * description: Unauthorized, invalid credentials or not authenticated - * 500: - * description: Some server error - */ -const changePassword = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const { oldPassword, newPassword, confirmPassword } = req.body; - const userId = req.user.id; - const { message } = await authService.changePassword( - userId, - oldPassword, - newPassword, - confirmPassword, - ); - res.status(200).json({ message }); - } catch (error) { - next(error); - } -}; - -/** - * @swagger - * /api/v1/auth/magic-link: - * post: - * tags: - * - Auth - * summary: Passwordless sign-in with email - * description: API endpoint to initiate passwordless sign-in by sending email to the registered user - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * email: - * type: string - * format: email - * example: user@example.com - * responses: - * 200: - * description: Sign-in token sent to email - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: Sign-in token sent to email - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 400 - * message: - * type: string - * example: Invalid request body - * 404: - * description: User not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 404 - * message: - * type: string - * example: User not found - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 500 - * message: - * type: string - * example: Internal server error - */ -const createMagicToken = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const email = req.body?.email; - if (!email) { - throw new BadRequest("Email is missing in request body."); - } - const response = await authService.generateMagicLink(email); - if (!response.ok) { - throw new BadRequest("Error processing request"); - } - const requestUtils = new RequestUtils(req, res); - requestUtils.addDataToState("localUser", response.user); - - return res.status(200).json({ - status_code: 200, - message: response.message, - }); - } catch (error) { - next(error); - } -}; - -/** - * @swagger - * /api/v1/auth/magic-link: - * get: - * tags: - * - Auth - * summary: Authenticate user with magic link - * description: Validates the magic link token and authenticates the user - * parameters: - * - in: query - * name: token - * required: true - * schema: - * type: string - * description: Magic link token - * - in: query - * name: redirect - * schema: - * type: boolean - * description: Whether to redirect after authentication (true/false) - * responses: - * 200: - * description: User authenticated successfully - * headers: - * Authorization: - * schema: - * type: string - * description: Bearer token for authentication - * Set-Cookie: - * schema: - * type: string - * description: Contains the hng_token - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: ok - * data: - * type: object - * properties: - * id: - * type: string - * example: user123 - * email: - * type: string - * example: user@example.com - * name: - * type: string - * example: John Doe - * access_token: - * type: string - * example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - * 302: - * description: Redirect to home page (when redirect=true) - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: error - * message: - * type: string - * example: Invalid Request - * 500: - * description: Internal server error - * security: [] - */ -const VerifyUserMagicLink = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const token = req.query.token; - const response = await authService.validateMagicLinkToken(token as string); - if (response.status !== "ok") { - throw new BadRequest("Invalid Request"); - } - const { access_token } = await authService.passwordlessLogin( - response.userId, - ); - - const requestUtils = new RequestUtils(req, res); - let user: User = requestUtils.getDataFromState("local_user"); - if (!user?.email && !user?.id) { - user = await User.findOne({ - where: { email: response.email }, - }); - } - - const responseData = { - id: user.id, - email: user.email, - name: user.name, - }; - - res.header("Authorization", access_token); - res.cookie("hng_token", access_token, { - maxAge: 24 * 60 * 60 * 1000, - httpOnly: true, - secure: config.NODE_ENV !== "development", - sameSite: config.NODE_ENV === "development" ? "lax" : "none", - path: "/", - }); - - if (req.query?.redirect === "true") { - return res.redirect("/"); - } else { - return res.status(200).json({ - status_code: 200, - data: responseData, - access_token, - }); - } - } catch (err) { - if (err instanceof BadRequest) { - return res.status(400).json({ status: "error", message: err.message }); - } - next(err); - } -}; - -const googleAuthCall = async (req: Request, res: Response) => { - try { - const { id_token } = req.body; - - // Verify the ID token from google - const userInfo = await verifyToken(id_token); - - // update user info - const user = await GoogleUserInfo(userInfo); - - // generate access token for the user - const token = jwt.sign({ userId: user.id }, config.TOKEN_SECRET, { - expiresIn: "1d", - }); - - // Return the JWT and User - res.json({ user, access_token: token }); - } catch (error) { - res.status(400).json({ error: "Authentication failed" }); - } -}; - -const enable2FA = async (req: Request, res: Response, next: NextFunction) => { - const { password } = req.body; - const user = req.user; - const { message, data } = await authService.enable2FA(user.id, password); - if (!message) { - return next(new BadRequest("Error enabling 2FA")); - } - - return res.status(200).json({ - status_code: 200, - message, - data, - }); -}; - -const verify2FA = async (req: Request, res: Response, next: NextFunction) => { - const { totp_code } = req.body; - const user = req.user; - if (!user.is_2fa_enabled) { - return next(new BadRequest("2FA is not enabled")); - } - const is_verified = authService.verify2FA(totp_code, user); - if (!is_verified) { - return next(new Unauthorized("Invalid 2FA code")); - } - - return res.status(200).json({ - status_code: 200, - message: "2FA code verified", - data: { verified: true }, - }); -}; - -export { - VerifyUserMagicLink, - changePassword, - createMagicToken, - forgotPassword, - googleAuthCall, - // handleGoogleAuth, - login, - resetPassword, - signUp, - verifyOtp, - enable2FA, - verify2FA, -}; +import { NextFunction, Request, Response } from "express"; +import jwt from "jsonwebtoken"; +import config from "../config"; +import { verifyToken } from "../config/google.passport.config"; +import { BadRequest, Unauthorized } from "../middleware"; +import { User } from "../models"; +import { AuthService } from "../services/auth.services"; +import { GoogleUserInfo } from "../services/google.auth.service"; +import RequestUtils from "../utils/request.utils"; + +const authService = new AuthService(); + +/** + * @swagger + * tags: + * name: Auth + * description: Authentication related routes + */ + +/** + * @swagger + * api/v1/auth/register: + * post: + * summary: Sign up a new user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * first_name: + * type: string + * last_name: + * type: string + * email: + * type: string + * password: + * type: string + + * responses: + * 201: + * description: The user was successfully created + * content: + * application/json: + * schema: + * type: object + * properties: + * mailSent: + * type: string + * newUser: + * type: object + * access_token: + * type: string + * 409: + * description: User already exists + * 500: + * description: Some server error + */ + +const signUp = async (req: Request, res: Response, next: NextFunction) => { + try { + const { message, user, access_token } = await authService.signUp(req.body); + res.status(201).json({ message, user, access_token }); + } catch (error) { + console.log(error); + next(error); + } +}; + +/** + * @swagger + * /api/v1/auth/verify-otp: + * post: + * summary: Verify the user's email using OTP + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * otp: + * type: integer + * description: The OTP sent to the user's email + * token: + * type: string + * description: The token received during sign up + * responses: + * 200: + * description: OTP verified successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: Success message + * 400: + * description: Invalid OTP or verification token has expired + * 404: + * description: User not found + * 500: + * description: Some server error + */ + +const verifyOtp = async (req: Request, res: Response, next: NextFunction) => { + try { + const { otp, token } = req.body; + const { message } = await authService.verifyEmail(token as string, otp); + res.status(200).json({ message }); + } catch (error) { + next(error); + } +}; + +/** + * @swagger + * /api/v1/auth/login: + * post: + * summary: Log in a user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * password: + * type: string + * responses: + * 200: + * description: Successful login + * content: + * application/json: + * schema: + * type: object + * properties: + * access_token: + * type: string + * user: + * type: object + * 401: + * description: Invalid credentials + * 500: + * description: Some server error + */ + +const login = async (req: Request, res: Response, next: NextFunction) => { + try { + const { access_token, user } = await authService.login(req.body); + res.status(200).json({ access_token, user }); + } catch (error) { + next(error); + } +}; + +/** + * @swagger + * /api/v1/auth/forgotPassword: + * post: + * summary: Request a password reset + * description: Allows a user to request a password reset link by providing their email address. + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * properties: + * email: + * type: string + * description: The email address of the user. + * example: user@example.com + * responses: + * 200: + * description: Successfully requested password reset. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * description: The status of the request. + * example: success + * status_code: + * type: integer + * description: The HTTP status code. + * example: 200 + * message: + * type: string + * description: The message indicating the result of the request. + * example: Password reset link sent to your email. + * 400: + * description: Bad request + * 500: + * description: Internal server error. + */ + +const forgotPassword = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { email } = req.body; + const resetURL = `${req.protocol}://${req.get("host")}/${config["api-prefix"]}/auth/reset-password/`; + const { message } = await authService.forgotPassword(email, resetURL); + + res.status(200).json({ status: "sucess", status_code: 200, message }); + } catch (error) { + next(error); + } +}; + +/** + * @swagger + * /api/v1/auth/reset-password/:token: + * post: + * summary: Reset a user's password + * description: Allows a user to reset their password by providing a valid reset token and a new password. + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - token + * - newPassword + * properties: + * token: + * type: string + * description: The password reset token. + * example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + * newPassword: + * type: string + * description: The new password for the user. + * example: new_secure_password + * responses: + * 200: + * description: Successfully reset password. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: The message indicating the result of the password reset. + * example: Password has been reset successfully. + * 400: + * description: Bad request. + * 500: + * description: Internal server error. + */ + +const resetPassword = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { token } = req.params; + const { newPassword } = req.body; + const { message } = await authService.resetPassword(token, newPassword); + res.status(200).json({ status: "success", status_code: 200, message }); + } catch (error) { + next(error); + } +}; +/** + * @swagger + * /api/v1/auth/change-password: + * patch: + * summary: Change user password + * tags: [Auth] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * oldPassword: + * type: string + * description: Current password of the user + * newPassword: + * type: string + * description: New password to set for the user + * confirmPassword: + * type: string + * description: Confirmation of the new password + * responses: + * 200: + * description: Password changed successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Password changed successfully." + * 400: + * description: Bad request, such as mismatched passwords or invalid input + * 401: + * description: Unauthorized, invalid credentials or not authenticated + * 500: + * description: Some server error + */ +const changePassword = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { oldPassword, newPassword, confirmPassword } = req.body; + const userId = req.user.id; + const { message } = await authService.changePassword( + userId, + oldPassword, + newPassword, + confirmPassword, + ); + res.status(200).json({ message }); + } catch (error) { + next(error); + } +}; + +/** + * @swagger + * /api/v1/auth/magic-link: + * post: + * tags: + * - Auth + * summary: Passwordless sign-in with email + * description: API endpoint to initiate passwordless sign-in by sending email to the registered user + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * format: email + * example: user@example.com + * responses: + * 200: + * description: Sign-in token sent to email + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: Sign-in token sent to email + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 400 + * message: + * type: string + * example: Invalid request body + * 404: + * description: User not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 404 + * message: + * type: string + * example: User not found + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 500 + * message: + * type: string + * example: Internal server error + */ +const createMagicToken = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const email = req.body?.email; + if (!email) { + throw new BadRequest("Email is missing in request body."); + } + const response = await authService.generateMagicLink(email); + if (!response.ok) { + throw new BadRequest("Error processing request"); + } + const requestUtils = new RequestUtils(req, res); + requestUtils.addDataToState("localUser", response.user); + + return res.status(200).json({ + status_code: 200, + message: response.message, + }); + } catch (error) { + next(error); + } +}; + +/** + * @swagger + * /api/v1/auth/magic-link: + * get: + * tags: + * - Auth + * summary: Authenticate user with magic link + * description: Validates the magic link token and authenticates the user + * parameters: + * - in: query + * name: token + * required: true + * schema: + * type: string + * description: Magic link token + * - in: query + * name: redirect + * schema: + * type: boolean + * description: Whether to redirect after authentication (true/false) + * responses: + * 200: + * description: User authenticated successfully + * headers: + * Authorization: + * schema: + * type: string + * description: Bearer token for authentication + * Set-Cookie: + * schema: + * type: string + * description: Contains the hng_token + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: ok + * data: + * type: object + * properties: + * id: + * type: string + * example: user123 + * email: + * type: string + * example: user@example.com + * name: + * type: string + * example: John Doe + * access_token: + * type: string + * example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + * 302: + * description: Redirect to home page (when redirect=true) + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Invalid Request + * 500: + * description: Internal server error + * security: [] + */ +const VerifyUserMagicLink = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const token = req.query.token; + const response = await authService.validateMagicLinkToken(token as string); + if (response.status !== "ok") { + throw new BadRequest("Invalid Request"); + } + const { access_token } = await authService.passwordlessLogin( + response.userId, + ); + + const requestUtils = new RequestUtils(req, res); + let user: User = requestUtils.getDataFromState("local_user"); + if (!user?.email && !user?.id) { + user = await User.findOne({ + where: { email: response.email }, + }); + } + + const responseData = { + id: user.id, + email: user.email, + name: user.name, + }; + + res.header("Authorization", access_token); + res.cookie("hng_token", access_token, { + maxAge: 24 * 60 * 60 * 1000, + httpOnly: true, + secure: config.NODE_ENV !== "development", + sameSite: config.NODE_ENV === "development" ? "lax" : "none", + path: "/", + }); + + if (req.query?.redirect === "true") { + return res.redirect("/"); + } else { + return res.status(200).json({ + status_code: 200, + data: responseData, + access_token, + }); + } + } catch (err) { + if (err instanceof BadRequest) { + return res.status(400).json({ status: "error", message: err.message }); + } + next(err); + } +}; + +const googleAuthCall = async (req: Request, res: Response) => { + try { + const { id_token } = req.body; + + // Verify the ID token from google + const userInfo = await verifyToken(id_token); + + // update user info + const user = await GoogleUserInfo(userInfo); + + // generate access token for the user + const token = jwt.sign({ userId: user.id }, config.TOKEN_SECRET, { + expiresIn: "1d", + }); + + // Return the JWT and User + res.json({ user, access_token: token }); + } catch (error) { + res.status(400).json({ error: "Authentication failed" }); + } +}; + +const enable2FA = async (req: Request, res: Response, next: NextFunction) => { + const { password } = req.body; + const user = req.user; + const { message, data } = await authService.enable2FA(user.id, password); + if (!message) { + return next(new BadRequest("Error enabling 2FA")); + } + + return res.status(200).json({ + status_code: 200, + message, + data, + }); +}; + +const verify2FA = async (req: Request, res: Response, next: NextFunction) => { + const { totp_code } = req.body; + const user = req.user; + if (!user.is_2fa_enabled) { + return next(new BadRequest("2FA is not enabled")); + } + const is_verified = authService.verify2FA(totp_code, user); + if (!is_verified) { + return next(new Unauthorized("Invalid 2FA code")); + } + + return res.status(200).json({ + status_code: 200, + message: "2FA code verified", + data: { verified: true }, + }); +}; + +export { + VerifyUserMagicLink, + changePassword, + createMagicToken, + forgotPassword, + googleAuthCall, + // handleGoogleAuth, + login, + resetPassword, + signUp, + verifyOtp, + enable2FA, + verify2FA, +}; diff --git a/src/controllers/GoogleAuthController.ts b/src/controllers/GoogleAuthController.ts index bc5d8908..5b7f7aa6 100644 --- a/src/controllers/GoogleAuthController.ts +++ b/src/controllers/GoogleAuthController.ts @@ -1,115 +1,115 @@ -import passport from "../config/google.passport.config"; - -/** - * @swagger - * api/v1/auth/google: - * get: - * summary: Initiate Google OAuth authentication - * description: This endpoint initiates the OAuth 2.0 authentication process with Google. It redirects the user to the Google login page where they can authenticate. Upon successful authentication, Google will redirect the user back to the specified callback URL. - * tags: - * - Auth - * responses: - * 302: - * description: Redirect to Google's OAuth 2.0 login page - * headers: - * Location: - * description: The URL to which the user is being redirected - * schema: - * type: string - * example: "https://accounts.google.com/o/oauth2/auth" - * 500: - * description: Internal Server Error - An error occurred during the initiation of the authentication process - */ - -export const initiateGoogleAuthRequest = passport.authenticate("google", { - scope: ["openid", "email", "profile"], -}); - -/** - * @swagger - * api/v1/auth/google/callback: - * post: - * summary: Google OAuth callback - * description: This endpoint handles the callback from Google's OAuth2.0 authentication. It processes the user information and generates a JSON Web Token (JWT) for authenticated users. - * tags: - * - Auth - * parameters: - * - in: body - * name: body - * description: The body contains user information returned by Google after successful authentication. - * required: true - * schema: - * type: object - * properties: - * user: - * type: object - * properties: - * id: - * type: string - * description: The unique identifier for the user. - * example: "117189586949299940593" - * email: - * type: string - * description: The email address of the user. - * example: "example@gmail.com" - * name: - * type: string - * description: The full name of the user. - * example: "John Doe" - * picture: - * type: string - * description: URL to the user's profile picture. - * example: "https://example.com/profile.jpg" - * responses: - * 200: - * description: Successful authentication - * content: - * application/json: - * schema: - * type: object - * properties: - * access_token: - * type: string - * description: JWT token for authenticated user. - * user: - * type: object - * properties: - * id: - * type: string - * email: - * type: string - * name: - * type: string - * picture: - * type: string - * 401: - * description: Unauthorized - Authentication failed - * 500: - * description: Internal Server Error - An error occurred during the authentication process - */ -// export const googleAuthCallback = ( -// req: Request, -// res: Response, -// next: NextFunction, -// ) => { -// const authenticate = passport.authenticate( -// "google", -// async (error, user, info) => { -// const googleAuthService = new GoogleAuthService(); -// try { -// if (error) { -// throw new ServerError("Authentication error"); -// } -// if (!user) { -// throw new Unauthorized("Authentication failed!"); -// } -// const isDbUser = await googleAuthService.getUserByGoogleId(user.id); -// const dbUser = await googleAuthService.handleGoogleAuth(user, isDbUser); -// res.status(200).json(dbUser); -// } catch (error) { -// next(error); -// } -// }, -// ); -// authenticate(req, res, next); -// }; +import passport from "../config/google.passport.config"; + +/** + * @swagger + * api/v1/auth/google: + * get: + * summary: Initiate Google OAuth authentication + * description: This endpoint initiates the OAuth 2.0 authentication process with Google. It redirects the user to the Google login page where they can authenticate. Upon successful authentication, Google will redirect the user back to the specified callback URL. + * tags: + * - Auth + * responses: + * 302: + * description: Redirect to Google's OAuth 2.0 login page + * headers: + * Location: + * description: The URL to which the user is being redirected + * schema: + * type: string + * example: "https://accounts.google.com/o/oauth2/auth" + * 500: + * description: Internal Server Error - An error occurred during the initiation of the authentication process + */ + +export const initiateGoogleAuthRequest = passport.authenticate("google", { + scope: ["openid", "email", "profile"], +}); + +/** + * @swagger + * api/v1/auth/google/callback: + * post: + * summary: Google OAuth callback + * description: This endpoint handles the callback from Google's OAuth2.0 authentication. It processes the user information and generates a JSON Web Token (JWT) for authenticated users. + * tags: + * - Auth + * parameters: + * - in: body + * name: body + * description: The body contains user information returned by Google after successful authentication. + * required: true + * schema: + * type: object + * properties: + * user: + * type: object + * properties: + * id: + * type: string + * description: The unique identifier for the user. + * example: "117189586949299940593" + * email: + * type: string + * description: The email address of the user. + * example: "example@gmail.com" + * name: + * type: string + * description: The full name of the user. + * example: "John Doe" + * picture: + * type: string + * description: URL to the user's profile picture. + * example: "https://example.com/profile.jpg" + * responses: + * 200: + * description: Successful authentication + * content: + * application/json: + * schema: + * type: object + * properties: + * access_token: + * type: string + * description: JWT token for authenticated user. + * user: + * type: object + * properties: + * id: + * type: string + * email: + * type: string + * name: + * type: string + * picture: + * type: string + * 401: + * description: Unauthorized - Authentication failed + * 500: + * description: Internal Server Error - An error occurred during the authentication process + */ +// export const googleAuthCallback = ( +// req: Request, +// res: Response, +// next: NextFunction, +// ) => { +// const authenticate = passport.authenticate( +// "google", +// async (error, user, info) => { +// const googleAuthService = new GoogleAuthService(); +// try { +// if (error) { +// throw new ServerError("Authentication error"); +// } +// if (!user) { +// throw new Unauthorized("Authentication failed!"); +// } +// const isDbUser = await googleAuthService.getUserByGoogleId(user.id); +// const dbUser = await googleAuthService.handleGoogleAuth(user, isDbUser); +// res.status(200).json(dbUser); +// } catch (error) { +// next(error); +// } +// }, +// ); +// authenticate(req, res, next); +// }; diff --git a/src/controllers/HelpController.ts b/src/controllers/HelpController.ts index 30b0d098..80eea846 100644 --- a/src/controllers/HelpController.ts +++ b/src/controllers/HelpController.ts @@ -1,432 +1,432 @@ -// src/controllers/UserController.ts -import { Request, Response } from "express"; -import { HelpService } from "../services/help.services"; -import { HttpError } from "../middleware"; - -/** - * @swagger - * tags: - * name: HelpCenter - * description: Help Center related routes - */ - -/** - * @swagger - * /api/v1/help-center/: - * post: - * summary: SuperAdmin- Create a new help center topic - * tags: [HelpCenter] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - title - * - content - * - author - * properties: - * title: - * type: string - * content: - * type: string - * author: - * type: string - * responses: - * 201: - * description: Topic Created Successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * data: - * type: object - * properties: - * article_id: - * type: string - * title: - * type: string - * content: - * type: string - * author: - * type: string - * createdAt: - * type: string - * format: date-time - * updatedAt: - * type: string - * format: date-time - * status_code: - * type: integer - * example: 201 - * 422: - * description: Validation failed - * 500: - * description: Internal Server Error - */ - -/** - * @swagger - * /api/v1/help-center/topics: - * get: - * summary: Get all help center topics - * tags: [HelpCenter] - * responses: - * 201: - * description: Fetch Successful - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * data: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * title: - * type: string - * content: - * type: string - * author: - * type: string - * createdAt: - * type: string - * format: date-time - * updatedAt: - * type: string - * format: date-time - * status_code: - * type: integer - * example: 201 - * 500: - * description: Internal Server Error - */ - -/** - * @swagger - * /api/v1/help-center/topics/{id}: - * get: - * summary: Get a help center topic by ID - * tags: [HelpCenter] - * parameters: - * - in: path - * name: id - * schema: - * type: string - * required: true - * description: The topic ID - * responses: - * 201: - * description: Fetch Successful - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * data: - * type: object - * properties: - * id: - * type: string - * title: - * type: string - * content: - * type: string - * author: - * type: string - * createdAt: - * type: string - * format: date-time - * updatedAt: - * type: string - * format: date-time - * status_code: - * type: integer - * example: 201 - * 422: - * description: Validation failed - * 404: - * description: Not Found - * 500: - * description: Internal Server Error - */ - -/** - * @swagger - * /api/v1/help-center/topic/{:id}: - * delete: - * summary: SuperAdmin- Delete a help center topic by ID - * tags: [HelpCenter] - * parameters: - * - in: path - * name: id - * schema: - * type: string - * required: true - * description: The topic ID - * responses: - * 201: - * description: Delete Successful - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * status_code: - * type: integer - * example: 201 - * 422: - * description: Validation failed - * 403: - * description: Access denied! You are not an admin - * 404: - * description: Not Found - * 500: - * description: Internal Server Error - */ - -/** - * @swagger - * /api/v1/help-center/topic/{id}: - * patch: - * summary: SuperAdmin- Update a help center topic by ID - * tags: [HelpCenter] - * parameters: - * - in: path - * name: id - * schema: - * type: string - * required: true - * description: The topic ID - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - title - * - content - * - author - * properties: - * title: - * type: string - * content: - * type: string - * author: - * type: string - * responses: - * 200: - * description: Topic Updated Successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * data: - * type: object - * properties: - * article_id: - * type: string - * title: - * type: string - * content: - * type: string - * author: - * type: string - * createdAt: - * type: string - * format: date-time - * updatedAt: - * type: string - * format: date-time - * status_code: - * type: integer - * example: 200 - * 422: - * description: Validation failed - * 403: - * description: Access denied! You are not an admin - * 404: - * description: Not Found - * 500: - * description: Internal Server Error - */ - -class HelpController { - private helpService: HelpService; - - constructor() { - this.helpService = new HelpService(); - } - - async createTopic(req: Request, res: Response): Promise { - try { - const { title, content, author } = req.body; - - //Validate Input - if (!title || !content || !author) { - throw new HttpError( - 422, - "Validation failed: Title, content, and author are required", - ); - } - - const topic = await this.helpService.create(title, content, author); - res.status(201).json({ - success: true, - message: "Topic Created Successfully", - data: { - article_id: topic.id, - content: topic.content, - author: topic.author, - title: topic.title, - createdAt: topic.createdAt, - updatedAt: topic.updatedAt, - }, - status_code: 201, - }); - } catch (error) { - if (error instanceof HttpError) { - res.status(error.status_code).json({ message: error.message }); - } else { - res - .status(error.status_code || 500) - .json({ message: error.message || "Internal Server Error" }); - } - } - } - - async getAllTopics(req: Request, res: Response): Promise { - try { - const topics = await this.helpService.getAll(); - res.status(200).json({ - success: true, - message: "Fetch Successful", - data: topics, - status_code: 200, - }); - } catch (error) { - if (error instanceof HttpError) { - res.status(error.status_code).json({ message: error.message }); - } else { - res - .status(error.status_code || 500) - .json({ message: error.message || "Internal Server Error" }); - } - } - } - - async getTopicById(req: Request, res: Response): Promise { - try { - const id = req.params.id; - - //Validate Input - if (!id) { - throw new HttpError(422, "Validation failed: Valid ID required"); - } - const topic = await this.helpService.getTopicById(id); - res.status(201).json({ - success: true, - message: "Fetch Successful", - data: topic, - status_code: 201, - }); - } catch (error) { - if (error instanceof HttpError) { - res.status(error.status_code).json({ message: error.message }); - } else { - res - .status(error.status_code || 500) - .json({ message: error.message || "Internal Server Error" }); - } - } - } - - async deleteTopic(req: Request, res: Response): Promise { - try { - const id = req.params.id; - - //Validate Input - if (!id) { - throw new HttpError(422, "Validation failed: Valid ID required"); - } - await this.helpService.delete(id); - res.status(202).json({ - success: true, - message: "Delete Successful", - status_code: 202, - }); - } catch (error) { - if (error instanceof HttpError) { - res.status(error.status_code).json({ message: error.message }); - } else { - res - .status(error.status_code || 500) - .json({ message: error.message || "Internal Server Error" }); - } - } - } - - async updateTopic(req: Request, res: Response): Promise { - try { - const { title, content, author } = req.body; - const id = req.params.id; - - //Validate ID - if (!id) { - throw new HttpError(422, "Invalid topic id"); - } - - const topic = await this.helpService.update(id, title, content, author); - res.status(200).json({ - success: true, - message: "Topic Updated Successfully", - data: { - article_id: topic.id, - content: topic.content, - author: topic.author, - title: topic.title, - createdAt: topic.createdAt, - updatedAt: topic.updatedAt, - }, - status_code: 200, - }); - } catch (error) { - if (error instanceof HttpError) { - res.status(error.status_code).json({ message: error.message }); - } else { - res - .status(500) - .json({ message: error.message || "Internal Server Error" }); - } - } - } -} - -export default HelpController; +// src/controllers/UserController.ts +import { Request, Response } from "express"; +import { HelpService } from "../services/help.services"; +import { HttpError } from "../middleware"; + +/** + * @swagger + * tags: + * name: HelpCenter + * description: Help Center related routes + */ + +/** + * @swagger + * /api/v1/help-center/: + * post: + * summary: SuperAdmin- Create a new help center topic + * tags: [HelpCenter] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - title + * - content + * - author + * properties: + * title: + * type: string + * content: + * type: string + * author: + * type: string + * responses: + * 201: + * description: Topic Created Successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * article_id: + * type: string + * title: + * type: string + * content: + * type: string + * author: + * type: string + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * status_code: + * type: integer + * example: 201 + * 422: + * description: Validation failed + * 500: + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/help-center/topics: + * get: + * summary: Get all help center topics + * tags: [HelpCenter] + * responses: + * 201: + * description: Fetch Successful + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * title: + * type: string + * content: + * type: string + * author: + * type: string + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * status_code: + * type: integer + * example: 201 + * 500: + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/help-center/topics/{id}: + * get: + * summary: Get a help center topic by ID + * tags: [HelpCenter] + * parameters: + * - in: path + * name: id + * schema: + * type: string + * required: true + * description: The topic ID + * responses: + * 201: + * description: Fetch Successful + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * id: + * type: string + * title: + * type: string + * content: + * type: string + * author: + * type: string + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * status_code: + * type: integer + * example: 201 + * 422: + * description: Validation failed + * 404: + * description: Not Found + * 500: + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/help-center/topic/{:id}: + * delete: + * summary: SuperAdmin- Delete a help center topic by ID + * tags: [HelpCenter] + * parameters: + * - in: path + * name: id + * schema: + * type: string + * required: true + * description: The topic ID + * responses: + * 201: + * description: Delete Successful + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * status_code: + * type: integer + * example: 201 + * 422: + * description: Validation failed + * 403: + * description: Access denied! You are not an admin + * 404: + * description: Not Found + * 500: + * description: Internal Server Error + */ + +/** + * @swagger + * /api/v1/help-center/topic/{id}: + * patch: + * summary: SuperAdmin- Update a help center topic by ID + * tags: [HelpCenter] + * parameters: + * - in: path + * name: id + * schema: + * type: string + * required: true + * description: The topic ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - title + * - content + * - author + * properties: + * title: + * type: string + * content: + * type: string + * author: + * type: string + * responses: + * 200: + * description: Topic Updated Successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * article_id: + * type: string + * title: + * type: string + * content: + * type: string + * author: + * type: string + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * status_code: + * type: integer + * example: 200 + * 422: + * description: Validation failed + * 403: + * description: Access denied! You are not an admin + * 404: + * description: Not Found + * 500: + * description: Internal Server Error + */ + +class HelpController { + private helpService: HelpService; + + constructor() { + this.helpService = new HelpService(); + } + + async createTopic(req: Request, res: Response): Promise { + try { + const { title, content, author } = req.body; + + //Validate Input + if (!title || !content || !author) { + throw new HttpError( + 422, + "Validation failed: Title, content, and author are required", + ); + } + + const topic = await this.helpService.create(title, content, author); + res.status(201).json({ + success: true, + message: "Topic Created Successfully", + data: { + article_id: topic.id, + content: topic.content, + author: topic.author, + title: topic.title, + createdAt: topic.createdAt, + updatedAt: topic.updatedAt, + }, + status_code: 201, + }); + } catch (error) { + if (error instanceof HttpError) { + res.status(error.status_code).json({ message: error.message }); + } else { + res + .status(error.status_code || 500) + .json({ message: error.message || "Internal Server Error" }); + } + } + } + + async getAllTopics(req: Request, res: Response): Promise { + try { + const topics = await this.helpService.getAll(); + res.status(200).json({ + success: true, + message: "Fetch Successful", + data: topics, + status_code: 200, + }); + } catch (error) { + if (error instanceof HttpError) { + res.status(error.status_code).json({ message: error.message }); + } else { + res + .status(error.status_code || 500) + .json({ message: error.message || "Internal Server Error" }); + } + } + } + + async getTopicById(req: Request, res: Response): Promise { + try { + const id = req.params.id; + + //Validate Input + if (!id) { + throw new HttpError(422, "Validation failed: Valid ID required"); + } + const topic = await this.helpService.getTopicById(id); + res.status(201).json({ + success: true, + message: "Fetch Successful", + data: topic, + status_code: 201, + }); + } catch (error) { + if (error instanceof HttpError) { + res.status(error.status_code).json({ message: error.message }); + } else { + res + .status(error.status_code || 500) + .json({ message: error.message || "Internal Server Error" }); + } + } + } + + async deleteTopic(req: Request, res: Response): Promise { + try { + const id = req.params.id; + + //Validate Input + if (!id) { + throw new HttpError(422, "Validation failed: Valid ID required"); + } + await this.helpService.delete(id); + res.status(202).json({ + success: true, + message: "Delete Successful", + status_code: 202, + }); + } catch (error) { + if (error instanceof HttpError) { + res.status(error.status_code).json({ message: error.message }); + } else { + res + .status(error.status_code || 500) + .json({ message: error.message || "Internal Server Error" }); + } + } + } + + async updateTopic(req: Request, res: Response): Promise { + try { + const { title, content, author } = req.body; + const id = req.params.id; + + //Validate ID + if (!id) { + throw new HttpError(422, "Invalid topic id"); + } + + const topic = await this.helpService.update(id, title, content, author); + res.status(200).json({ + success: true, + message: "Topic Updated Successfully", + data: { + article_id: topic.id, + content: topic.content, + author: topic.author, + title: topic.title, + createdAt: topic.createdAt, + updatedAt: topic.updatedAt, + }, + status_code: 200, + }); + } catch (error) { + if (error instanceof HttpError) { + res.status(error.status_code).json({ message: error.message }); + } else { + res + .status(500) + .json({ message: error.message || "Internal Server Error" }); + } + } + } +} + +export default HelpController; diff --git a/src/controllers/NewsLetterSubscriptionController.ts b/src/controllers/NewsLetterSubscriptionController.ts index 9bb5a869..71b93edd 100644 --- a/src/controllers/NewsLetterSubscriptionController.ts +++ b/src/controllers/NewsLetterSubscriptionController.ts @@ -1,307 +1,307 @@ -import { NextFunction, Request, Response } from "express"; -import { BadRequest } from "../middleware"; -import { NewsLetterSubscriptionService } from "../services/newsLetterSubscription.service"; - -const newsLetterSubscriptionService = new NewsLetterSubscriptionService(); - -/** - * @swagger - * /api/v1/newsletter-subscription: - * post: - * summary: Subscribe to the newsletter - * description: Allows a user to subscribe to the newsletter by providing an email address. - * tags: - * - Newsletter - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * email: - * type: string - * format: email - * example: user@example.com - * description: The user's email address for subscribing to the newsletter. - * responses: - * 201: - * description: Subscription successful. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * message: - * type: string - * example: Subscriber subscription successful - * 200: - * description: User is already subscribed to the newsletter. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * message: - * type: string - * example: You are already subscribed to our newsletter. - * - * 400: - * description: User is already subscribed but unsubscribe. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: false - * status_code: - * type: number - * example: 400 - * message: - * type: string - * example: You are already subscribed, please enable newsletter subscription to receive newsletter again - * - * 500: - * description: Internal server error. An error occurred while processing the subscription. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: boolean - * example: false - * status_code: - * type: number - * example: 500 - * message: - * type: string - * example: An error occurred while processing your request. - */ -const subscribeToNewsletter = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - try { - const { email } = req.body; - if (!email) { - throw new BadRequest("Email is missing in request body."); - } - const subscriber = await newsLetterSubscriptionService.subscribeUser(email); - res.status(subscriber.isNewlySubscribe ? 201 : 200).json({ - status: "success", - message: subscriber.isNewlySubscribe - ? "Subscriber subscription successful" - : "You are already subscribed to our newsletter", - }); - } catch (error) { - console.log(error); - - next(error); - } -}; - -/** - * @swagger - * /newsletter/unsubscribe: - * post: - * summary: Unsubscribe from newsletter - * description: Allows a logedegin user to unsubscribe from the newsletter using their email address. - * tags: - * - Newsletter - * security: - * - bearerAuth: [] # Assumes you're using bearer token authentication - * responses: - * 200: - * description: Successfully unsubscribed from the newsletter. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * message: - * type: string - * example: Successfully unsubscribed from newsletter - * 400: - * description: Bad request, missing or invalid email. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: number - * example: 400 - * message: - * type: string - * example: You already unsubscribed to newsletter. - * 404: - * description: User not subscribed ti newsletter. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: number - * example: 404 - * message: - * type: string - * example: You are not subscribed to newsletter. - */ -const unSubscribeToNewsletter = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const { email } = req.user; - if (!email) { - throw new BadRequest("Email is missing in request body."); - } - const subscriber = - await newsLetterSubscriptionService.unSubcribeUser(email); - if (subscriber) { - res.status(200).json({ - status: "success", - message: "Successfully unsubscribed from newsletter", - }); - } - } catch (error) { - next(error); - } -}; - -/** - * @swagger - * /api/v1/newsletters: - * get: - * summary: Get all newsletters with pagination - * tags: [Newsletters] - * parameters: - * - in: query - * name: page - * schema: - * type: integer - * default: 1 - * description: The page number for pagination - * - in: query - * name: limit - * schema: - * type: integer - * default: 10 - * description: The number of items per page - * responses: - * 200: - * description: A list of newsletters with pagination metadata - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: "success" - * message: - * type: string - * example: "Newsletters retrieved successfully" - * data: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * example: "newsletterId123" - * title: - * type: string - * example: "Weekly Update" - * content: - * type: string - * example: "This is the content of the newsletter." - * meta: - * type: object - * properties: - * total: - * type: integer - * example: 100 - * page: - * type: integer - * example: 1 - * limit: - * type: integer - * example: 10 - * totalPages: - * type: integer - * example: 10 - * 400: - * description: Bad request, possibly due to invalid query parameters - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Invalid page or limit parameter" - * status_code: - * type: integer - * example: 400 - * 500: - * description: An error occurred while fetching the newsletters - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Internal server error" - * status_code: - * type: integer - * example: 500 - */ - -const getAllNewsletter = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const { page, limit } = req.query; - const { data, meta } = - await newsLetterSubscriptionService.fetchAllNewsletter({ - page: Number(page), - limit: Number(limit), - }); - - return res.status(200).json({ - status: "", - message: "", - data: data, - meta, - }); - } catch (error) { - next(error); - } -}; - -export { getAllNewsletter, subscribeToNewsletter, unSubscribeToNewsletter }; +import { NextFunction, Request, Response } from "express"; +import { BadRequest } from "../middleware"; +import { NewsLetterSubscriptionService } from "../services/newsLetterSubscription.service"; + +const newsLetterSubscriptionService = new NewsLetterSubscriptionService(); + +/** + * @swagger + * /api/v1/newsletter-subscription: + * post: + * summary: Subscribe to the newsletter + * description: Allows a user to subscribe to the newsletter by providing an email address. + * tags: + * - Newsletter + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * format: email + * example: user@example.com + * description: The user's email address for subscribing to the newsletter. + * responses: + * 201: + * description: Subscription successful. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * message: + * type: string + * example: Subscriber subscription successful + * 200: + * description: User is already subscribed to the newsletter. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * message: + * type: string + * example: You are already subscribed to our newsletter. + * + * 400: + * description: User is already subscribed but unsubscribe. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: false + * status_code: + * type: number + * example: 400 + * message: + * type: string + * example: You are already subscribed, please enable newsletter subscription to receive newsletter again + * + * 500: + * description: Internal server error. An error occurred while processing the subscription. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: false + * status_code: + * type: number + * example: 500 + * message: + * type: string + * example: An error occurred while processing your request. + */ +const subscribeToNewsletter = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const { email } = req.body; + if (!email) { + throw new BadRequest("Email is missing in request body."); + } + const subscriber = await newsLetterSubscriptionService.subscribeUser(email); + res.status(subscriber.isNewlySubscribe ? 201 : 200).json({ + status: "success", + message: subscriber.isNewlySubscribe + ? "Subscriber subscription successful" + : "You are already subscribed to our newsletter", + }); + } catch (error) { + console.log(error); + + next(error); + } +}; + +/** + * @swagger + * /newsletter/unsubscribe: + * post: + * summary: Unsubscribe from newsletter + * description: Allows a logedegin user to unsubscribe from the newsletter using their email address. + * tags: + * - Newsletter + * security: + * - bearerAuth: [] # Assumes you're using bearer token authentication + * responses: + * 200: + * description: Successfully unsubscribed from the newsletter. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * message: + * type: string + * example: Successfully unsubscribed from newsletter + * 400: + * description: Bad request, missing or invalid email. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: number + * example: 400 + * message: + * type: string + * example: You already unsubscribed to newsletter. + * 404: + * description: User not subscribed ti newsletter. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: number + * example: 404 + * message: + * type: string + * example: You are not subscribed to newsletter. + */ +const unSubscribeToNewsletter = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { email } = req.user; + if (!email) { + throw new BadRequest("Email is missing in request body."); + } + const subscriber = + await newsLetterSubscriptionService.unSubcribeUser(email); + if (subscriber) { + res.status(200).json({ + status: "success", + message: "Successfully unsubscribed from newsletter", + }); + } + } catch (error) { + next(error); + } +}; + +/** + * @swagger + * /api/v1/newsletters: + * get: + * summary: Get all newsletters with pagination + * tags: [Newsletters] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: The page number for pagination + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: The number of items per page + * responses: + * 200: + * description: A list of newsletters with pagination metadata + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * message: + * type: string + * example: "Newsletters retrieved successfully" + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * example: "newsletterId123" + * title: + * type: string + * example: "Weekly Update" + * content: + * type: string + * example: "This is the content of the newsletter." + * meta: + * type: object + * properties: + * total: + * type: integer + * example: 100 + * page: + * type: integer + * example: 1 + * limit: + * type: integer + * example: 10 + * totalPages: + * type: integer + * example: 10 + * 400: + * description: Bad request, possibly due to invalid query parameters + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Invalid page or limit parameter" + * status_code: + * type: integer + * example: 400 + * 500: + * description: An error occurred while fetching the newsletters + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Internal server error" + * status_code: + * type: integer + * example: 500 + */ + +const getAllNewsletter = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { page, limit } = req.query; + const { data, meta } = + await newsLetterSubscriptionService.fetchAllNewsletter({ + page: Number(page), + limit: Number(limit), + }); + + return res.status(200).json({ + status: "", + message: "", + data: data, + meta, + }); + } catch (error) { + next(error); + } +}; + +export { getAllNewsletter, subscribeToNewsletter, unSubscribeToNewsletter }; diff --git a/src/controllers/NotificationController.ts b/src/controllers/NotificationController.ts index 4b4b0d3e..41180b07 100644 --- a/src/controllers/NotificationController.ts +++ b/src/controllers/NotificationController.ts @@ -1,71 +1,71 @@ -import { Request, Response, NextFunction } from "express"; -import { NotificationsService } from "../services"; -import { User } from "../models"; -import { Repository } from "typeorm"; -import AppDataSource from "../data-source"; - -class NotificationController { - private notificationsService: NotificationsService; - private userRepository: Repository; - - constructor() { - this.notificationsService = new NotificationsService(); - this.userRepository = AppDataSource.getRepository(User); - } - - public getNotificationsForUser = async ( - req: Request, - res: Response, - next: NextFunction, - ): Promise => { - try { - const userId = req.user?.id; - if (!userId) { - res.status(400).json({ - status: "fail", - status_code: 400, - message: "User ID is required", - }); - return; - } - - const user = await this.userRepository.findOne({ where: { id: userId } }); - if (!user) { - res.status(404).json({ - status: "success", - status_code: 404, - message: "User not found!", - }); - return; - } - const notifications = - await this.notificationsService.getNotificationsForUser(userId); - - res.status(200).json({ - status: "success", - status_code: 200, - message: "Notifications retrieved successfully", - data: { - total_notification_count: notifications.totalNotificationCount, - total_unread_notification_count: - notifications.totalUnreadNotificationCount, - notifications: notifications.notifications.map( - ({ id, isRead, message, createdAt }) => ({ - notification_id: id, - is_read: isRead, - message, - created_at: createdAt, - }), - ), - }, - }); - } catch (error) { - res.status(500).json({ - status_code: 500, - error: error.message || "An unexpected error occurred", - }); - } - }; -} - -export { NotificationController }; +import { Request, Response, NextFunction } from "express"; +import { NotificationsService } from "../services"; +import { User } from "../models"; +import { Repository } from "typeorm"; +import AppDataSource from "../data-source"; + +class NotificationController { + private notificationsService: NotificationsService; + private userRepository: Repository; + + constructor() { + this.notificationsService = new NotificationsService(); + this.userRepository = AppDataSource.getRepository(User); + } + + public getNotificationsForUser = async ( + req: Request, + res: Response, + next: NextFunction, + ): Promise => { + try { + const userId = req.user?.id; + if (!userId) { + res.status(400).json({ + status: "fail", + status_code: 400, + message: "User ID is required", + }); + return; + } + + const user = await this.userRepository.findOne({ where: { id: userId } }); + if (!user) { + res.status(404).json({ + status: "success", + status_code: 404, + message: "User not found!", + }); + return; + } + const notifications = + await this.notificationsService.getNotificationsForUser(userId); + + res.status(200).json({ + status: "success", + status_code: 200, + message: "Notifications retrieved successfully", + data: { + total_notification_count: notifications.totalNotificationCount, + total_unread_notification_count: + notifications.totalUnreadNotificationCount, + notifications: notifications.notifications.map( + ({ id, isRead, message, createdAt }) => ({ + notification_id: id, + is_read: isRead, + message, + created_at: createdAt, + }), + ), + }, + }); + } catch (error) { + res.status(500).json({ + status_code: 500, + error: error.message || "An unexpected error occurred", + }); + } + }; +} + +export { NotificationController }; diff --git a/src/controllers/NotificationSettingsController.ts b/src/controllers/NotificationSettingsController.ts index 85f75277..98a49acf 100644 --- a/src/controllers/NotificationSettingsController.ts +++ b/src/controllers/NotificationSettingsController.ts @@ -1,239 +1,239 @@ -import { NotificationSetting } from "../models/notificationsettings"; -import { Request, Response } from "express"; -import { validate } from "class-validator"; - -/** - * @swagger - * /api/v1/settings/notification-settings: - * put: - * summary: Create or update user notification settings - * tags: [Notifications] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * email_notifications: - * type: boolean - * push_notifications: - * type: boolean - * sms_notifications: - * type: boolean - * responses: - * 200: - * description: Notification settings created or updated successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * code: - * type: number - * example: 200 - * data: - * type: object - * properties: - * id: - * type: number - * example: 1 - * user_id: - * type: number - * example: 123 - * email_notifications: - * type: boolean - * example: true - * push_notifications: - * type: boolean - * example: false - * sms_notifications: - * type: boolean - * example: true - * 400: - * description: Validation failed - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: error - * code: - * type: number - * example: 400 - * message: - * type: string - * example: Validation failed - * errors: - * type: array - * items: - * type: object - * 500: - * description: Server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: error - * code: - * type: number - * example: 500 - * message: - * type: string - * example: Error updating user notification settings - * error: - * type: string - */ - -const CreateOrUpdateNotification = async (req: Request, res: Response) => { - try { - const user_id = req.user.id; - const { email_notifications, push_notifications, sms_notifications } = - req.body; - - let notificationSetting = await NotificationSetting.findOne({ - where: { user_id }, - }); - - if (notificationSetting) { - // Update existing setting - notificationSetting.email_notifications = email_notifications; - notificationSetting.push_notifications = push_notifications; - notificationSetting.sms_notifications = sms_notifications; - } else { - // Create new setting - notificationSetting = NotificationSetting.create({ - user_id, - email_notifications, - push_notifications, - sms_notifications, - }); - } - - // Validate the notificationSetting entity - const errors = await validate(notificationSetting); - if (errors.length > 0) { - return res.status(400).json({ - status: "error", - code: 400, - message: "Validation failed", - errors: errors, - }); - } - - const result = await NotificationSetting.save(notificationSetting); - res.status(200).json({ status: "success", code: 200, data: result }); - } catch (error) { - res.status(500).json({ - status: "error", - code: 500, - message: "Error updating user notification settings", - error: error.message, - }); - } -}; - -/** - * @swagger - * api/v1/settings/notification-settings/{user_id}: - * get: - * summary: Get notification settings for a user - * tags: [Notifications] - * description: Retrieves the notification settings for a specific user - * parameters: - * - in: path - * name: user_id - * required: true - * description: ID of the user to get notification settings for - * schema: - * type: string - * responses: - * 200: - * description: Successful response - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * code: - * type: integer - * example: 200 - * data: - * type: object - * properties: - * user_id: - * type: string - * example: "123456" - * email_notifications: - * type: boolean - * example: true - * push_notifications: - * type: boolean - * example: false - * sms_notifications: - * type: boolean - * example: true - * 404: - * description: User not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: Not found - * message: - * type: string - * example: The user with the requested id cannot be found - * 500: - * description: Server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: error - * code: - * type: integer - * example: 500 - * message: - * type: string - * example: Internal server error - */ - -const GetNotification = async (req: Request, res: Response) => { - try { - const settings = await NotificationSetting.findOne({ - where: { user_id: String(req.params.user_id) }, - }); - if (settings === null) { - return res.status(404).json({ - status: "Not found", - message: "The user with the requested id cannot be found", - }); - } - res.status(200).json({ status: "success", code: 200, data: settings }); - } catch (error) { - res - .status(500) - .json({ status: "error", code: 500, message: error.message }); - } -}; - -export { CreateOrUpdateNotification, GetNotification }; +import { NotificationSetting } from "../models/notificationsettings"; +import { Request, Response } from "express"; +import { validate } from "class-validator"; + +/** + * @swagger + * /api/v1/settings/notification-settings: + * put: + * summary: Create or update user notification settings + * tags: [Notifications] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email_notifications: + * type: boolean + * push_notifications: + * type: boolean + * sms_notifications: + * type: boolean + * responses: + * 200: + * description: Notification settings created or updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * code: + * type: number + * example: 200 + * data: + * type: object + * properties: + * id: + * type: number + * example: 1 + * user_id: + * type: number + * example: 123 + * email_notifications: + * type: boolean + * example: true + * push_notifications: + * type: boolean + * example: false + * sms_notifications: + * type: boolean + * example: true + * 400: + * description: Validation failed + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * code: + * type: number + * example: 400 + * message: + * type: string + * example: Validation failed + * errors: + * type: array + * items: + * type: object + * 500: + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * code: + * type: number + * example: 500 + * message: + * type: string + * example: Error updating user notification settings + * error: + * type: string + */ + +const CreateOrUpdateNotification = async (req: Request, res: Response) => { + try { + const user_id = req.user.id; + const { email_notifications, push_notifications, sms_notifications } = + req.body; + + let notificationSetting = await NotificationSetting.findOne({ + where: { user_id }, + }); + + if (notificationSetting) { + // Update existing setting + notificationSetting.email_notifications = email_notifications; + notificationSetting.push_notifications = push_notifications; + notificationSetting.sms_notifications = sms_notifications; + } else { + // Create new setting + notificationSetting = NotificationSetting.create({ + user_id, + email_notifications, + push_notifications, + sms_notifications, + }); + } + + // Validate the notificationSetting entity + const errors = await validate(notificationSetting); + if (errors.length > 0) { + return res.status(400).json({ + status: "error", + code: 400, + message: "Validation failed", + errors: errors, + }); + } + + const result = await NotificationSetting.save(notificationSetting); + res.status(200).json({ status: "success", code: 200, data: result }); + } catch (error) { + res.status(500).json({ + status: "error", + code: 500, + message: "Error updating user notification settings", + error: error.message, + }); + } +}; + +/** + * @swagger + * api/v1/settings/notification-settings/{user_id}: + * get: + * summary: Get notification settings for a user + * tags: [Notifications] + * description: Retrieves the notification settings for a specific user + * parameters: + * - in: path + * name: user_id + * required: true + * description: ID of the user to get notification settings for + * schema: + * type: string + * responses: + * 200: + * description: Successful response + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * code: + * type: integer + * example: 200 + * data: + * type: object + * properties: + * user_id: + * type: string + * example: "123456" + * email_notifications: + * type: boolean + * example: true + * push_notifications: + * type: boolean + * example: false + * sms_notifications: + * type: boolean + * example: true + * 404: + * description: User not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Not found + * message: + * type: string + * example: The user with the requested id cannot be found + * 500: + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * code: + * type: integer + * example: 500 + * message: + * type: string + * example: Internal server error + */ + +const GetNotification = async (req: Request, res: Response) => { + try { + const settings = await NotificationSetting.findOne({ + where: { user_id: String(req.params.user_id) }, + }); + if (settings === null) { + return res.status(404).json({ + status: "Not found", + message: "The user with the requested id cannot be found", + }); + } + res.status(200).json({ status: "success", code: 200, data: settings }); + } catch (error) { + res + .status(500) + .json({ status: "error", code: 500, message: error.message }); + } +}; + +export { CreateOrUpdateNotification, GetNotification }; diff --git a/src/controllers/OrgController.ts b/src/controllers/OrgController.ts index df4e6595..c1a3a085 100644 --- a/src/controllers/OrgController.ts +++ b/src/controllers/OrgController.ts @@ -1,1608 +1,1608 @@ -import { NextFunction, Request, Response } from "express"; -import { PermissionCategory } from "../enums/permission-category.enum"; -import { - HttpError, - InvalidInput, - ResourceNotFound, - ServerError, -} from "../middleware"; -import { OrgService } from "../services/org.services"; -import log from "../utils/logger"; - -export class OrgController { - private orgService: OrgService; - constructor() { - this.orgService = new OrgService(); - } - - /** - * @swagger - * /api/v1/organizations: - * post: - * summary: Create a new organisation - * description: This endpoint allows a user to create a new organisation - * tags: [Organisation] - * operationId: createOrganisation - * security: - * - bearerAuth: [] - * requestBody: - * description: Organisation payload - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * example: My Organisation - * description: - * type: string - * example: This is a sample organisation. - * email: - * type: string - * example: name@gmail.com - * industry: - * type: string - * example: entertainment - * type: - * type: string - * example: music - * country: - * type: string - * example: Nigeria - * address: - * type: string - * example: 121 ikeja - * state: - * type: string - * example: Oyo - * required: - * - name - * - description - * - email - * - industry - * - type - * - country - * - address - * - state - * responses: - * '201': - * description: Organisation created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * message: - * type: string - * example: Organisation created successfully - * data: - * type: object - * properties: - * id: - * type: string - * example: "1" - * name: - * type: string - * example: My Organisation - * description: - * type: string - * example: This is a sample organisation. - * email: - * type: string - * example: abc@gmail.com - * industry: - * type: string - * example: entertainment - * type: - * type: string - * example: music - * country: - * type: string - * example: Nigeria - * address: - * type: string - * example: 121 ikeja - * state: - * type: string - * example: Oyo - * slug: - * type: string - * example: 86820688-fd94-4b58-9bdd-99a701714a77 - * owner_id: - * type: string - * example: 86820688-fd94-4b58-9bdd-99a701714a76 - * createdAt: - * type: string - * format: date-time - * updatedAt: - * type: string - * format: date-time - * status_code: - * type: integer - * example: 201 - * '400': - * description: Bad Request - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: error - * message: - * type: string - * example: Invalid input - * status_code: - * type: integer - * example: 400 - * '500': - * description: Internal Server Error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: error - * message: - * type: string - * example: Internal server error - * status_code: - * type: integer - * example: 500 - * components: - * securitySchemes: - * bearerAuth: - * type: http - * scheme: bearer - * bearerFormat: JWT - */ - - async createOrganisation(req: Request, res: Response, next: NextFunction) { - try { - const payload = req.body; - const user = req.user; - const userId = user.id; - - const organisationService = new OrgService(); - const new_organisation = await organisationService.createOrganisation( - payload, - userId, - ); - - const respObj = { - status: "success", - message: "organisation created successfully", - data: new_organisation, - status_code: 201, - }; - - return res.status(201).json(respObj); - } catch (error) { - next(error); - } - } - - /** - * @swagger - * /api/v1/users/{userId}/organizations: - * get: - * summary: Get user organizations - * description: Retrieve all organizations associated with a specific user - * tags: [Organisation] - * parameters: - * - in: path - * name: userId - * required: true - * schema: - * type: string - * description: The ID of the user - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Organizations retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: Organizations retrieved successfully. - * data: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * 400: - * description: Invalid user ID or authentication mismatch - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 400 - * message: - * type: string - * example: Invalid user ID or authentication mismatch. - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 500 - * message: - * type: string - * example: Failed to retrieve organizations. Please try again later. - */ - - async getOrganizations(req: Request, res: Response) { - try { - const userId = req.params.id; - log.info("req.user:", req.user); - if (!req.user || req.user.id !== userId) { - return res.status(400).json({ - status: "unsuccessful", - status_code: 400, - message: "Invalid user ID or authentication mismatch.", - }); - } - const organizations = - await this.orgService.getOrganizationsByUserId(userId); - - if (organizations.length === 0) { - return res.status(200).json({ - status: "success", - status_code: 200, - message: "No organizations found for this user.", - data: [], - }); - } - - res.status(200).json({ - status: "success", - status_code: 200, - message: "Organizations retrieved successfully.", - data: organizations, - }); - } catch (error) { - log.error("Failed to retrieve organizations:", error); - res.status(500).json({ - status: "unsuccessful", - status_code: 500, - message: "Failed to retrieve organizations. Please try again later.", - }); - } - } - - /** - * @swagger - * /api/v1/organizations/{org_id}: - * get: - * summary: Get a single organization - * description: Retrieve details of a specific organization by its ID - * tags: [Organisation] - * parameters: - * - in: path - * name: org_id - * required: true - * schema: - * type: string - * description: The ID of the organization - * responses: - * 200: - * description: Successful operation - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * status_code: - * type: integer - * example: 200 - * data: - * type: object - * properties: - * org_id: - * type: string - * example: "2928a3d6-2b85-4abc-9438-ff9769b126ed" - * name: - * type: string - * example: "Organisation 1" - * description: - * type: string - * example: "Description of the organisation" - * 404: - * description: Organization not found - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: forbidden - * message: - * type: string - * example: Organization not found - * status_code: - * type: integer - * example: 404 - * 500: - * description: Server error - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 500 - * message: - * type: string - * example: Failed to get user organisation. Please try again later. - */ - - async getSingleOrg(req: Request, res: Response) { - try { - const org = await this.orgService.getSingleOrg( - req.params.org_id, - req.user.id, - ); - if (!org) { - return res.status(404).json({ - status: "forbidden", - message: "Organization not found", - status_code: 404, - }); - } - - res.status(200).json({ - status: "success", - status_code: 200, - data: { - org_id: org.id, - name: org.name, - description: org.description, - }, - }); - } catch (error) { - res.status(500).json({ - status: "unsuccessful", - status_code: 500, - message: "Failed to get user organisation. Please try again later.", - }); - } - } - - /** - * @swagger - * /api/v1/organizations/{org_id}/user/{user_id}: - * delete: - * summary: Remove a user from an organization - * description: Delete a user from a specific organization by user ID and organization ID - * tags: [Organisation] - * parameters: - * - in: path - * name: org_id - * required: true - * schema: - * type: string - * description: The ID of the organization - * - in: path - * name: user_id - * required: true - * schema: - * type: string - * description: The ID of the user - * responses: - * 200: - * description: Successful operation - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: User deleted successfully - * 404: - * description: User not found in the organization - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: forbidden - * message: - * type: string - * example: User not found in the organization - * status_code: - * type: integer - * example: 404 - * 400: - * description: Failed to remove user from organization - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: Bad Request - * message: - * type: string - * example: Failed to remove user from organization - * status_code: - * type: integer - * example: 400 - */ - async removeUser(req: Request, res: Response) { - try { - const user = await this.orgService.removeUser( - req.params.org_id, - req.params.user_id, - ); - - if (!user) { - return res.status(404).json({ - status: "forbidden", - message: "User not found in the organization", - status_code: 404, - }); - } - res.status(200).json({ - status: "success", - message: "User deleted successfully", - status_code: 200, - }); - } catch (error) { - res.status(400).json({ - status: "Bad Request", - message: "Failed to remove user from organization", - status_code: "400", - }); - } - } - - /** - * @swagger - * /organizations/{organization_id}: - * put: - * summary: Update organization details - * description: Update the details of an existing organization - * parameters: - * - in: path - * name: organization_id - * required: true - * schema: - * type: string - * description: The ID of the organization to update - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * - email - * - industry - * - type - * - country - * - address - * - state - * - description - * properties: - * name: - * type: string - * example: "New Organization Name" - * email: - * type: string - * example: "newemail@example.com" - * industry: - * type: string - * example: "Tech" - * type: - * type: string - * example: "Private" - * country: - * type: string - * example: "NGA" - * address: - * type: string - * example: "1234 New HNG" - * state: - * type: string - * example: "Lagos" - * description: - * type: string - * example: "A new description of the organization." - * responses: - * 200: - * description: Organization updated successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: "success" - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: "Organisation updated successfully" - * data: - * type: object - * properties: - * organization_id: - * type: string - * example: "61202249-0bc4-41eb-8cd5-7b873b7c7cc7" - * name: - * type: string - * example: "New Organization Name" - * email: - * type: string - * example: "newemail@example.com" - * industry: - * type: string - * example: "Tech" - * type: - * type: string - * example: "Private" - * country: - * type: string - * example: "NGA" - * address: - * type: string - * example: "1234 New HNG" - * state: - * type: string - * example: "Lagos" - * description: - * type: string - * example: "A new description of the organization." - * 404: - * description: Organization not found - * 500: - * description: Failed to update organization details - */ - - async updateOrganisation(req: Request, res: Response, next: NextFunction) { - try { - const orgId = req.params.organization_id; - const payload = req.body; - const userId = req.user.id; - - const updatedOrganisation = - await this.orgService.updateOrganizationDetails(orgId, userId, payload); - - const { - id, - name, - email, - industry, - type, - country, - address, - state, - description, - } = updatedOrganisation; - - const respObj = { - status: "success", - status_code: 200, - message: "Organisation updated successfully", - data: { - organization_id: id, - name, - email, - industry, - type, - country, - address, - state, - description, - }, - }; - - return res.status(200).json(respObj); - } catch (error) { - if (error instanceof ResourceNotFound) { - next(error); - } else { - next(new HttpError(500, "Failed to update organization details")); - } - } - } - - /** - * @swagger - * /organizations/accept-invite: - * post: - * summary: Add user to organization using an invite token - * description: Adds a user to an organization using an invite token. The user must be registered to join the organization. - * tags: [Organization] - * parameters: - * - in: query - * name: token - * required: true - * schema: - * type: string - * description: The invitation token - * responses: - * 201: - * description: User added to organization successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 201 - * message: - * type: string - * example: User added to organization successfully - * 404: - * description: Invalid or expired invite token, or user not registered. - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 404 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: Invalid or expired invite token - * 409: - * description: User already added to organization. - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 409 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: User already added to organization - * 500: - * description: An unexpected error occurred while processing the request. - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 500 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: An unexpected error occurred - */ - - async addUserToOrganizationWithInvite( - req: Request, - res: Response, - next: NextFunction, - ) { - try { - const { token } = req.query; - const userId = req.user.id; - const message = await this.orgService.addUserToOrganizationWithInvite( - token as string, - userId, - ); - - res.status(201).json({ status_code: 201, message: message }); - } catch (error) { - res.status(500).json({ - status_code: 500, - success: false, - message: error.message || "An unexpected error occurred", - }); - } - } - - /** - * @swagger - * /organizations/{org_id}/invite: - * get: - * summary: Generate a generic invite link for an organization - * description: Generate a generic invite link that can be used to invite users to join the specified organization. The invite link is returned for sharing or use in invitation emails. - * tags: [Organization] - * parameters: - * - in: path - * name: org_id - * required: true - * schema: - * type: string - * description: The ID of the organization - * responses: - * 200: - * description: Generic invite link generated successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: Invite link generated successfully - * link: - * type: string - * example: "http://example.com/invite?token=abc123" - * 404: - * description: Organization not found. - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 404 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: Organization with ID {org_id} not found - * 500: - * description: An unexpected error occurred while processing the request. - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 500 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: An unexpected error occurred - */ - - async generateGenericInviteLink( - req: Request, - res: Response, - next: NextFunction, - ) { - try { - const link = await this.orgService.generateGenericInviteLink( - req.params.org_id, - ); - if (link) { - res.status(200).json({ - status_code: 200, - message: "Invite link generated successfully", - link, - }); - } - } catch (error) { - res.status(500).json({ - status_code: 500, - success: false, - message: error.message || "An unexpected error occurred", - }); - } - } - - /** - * @swagger - * /organizations/{org_id}/send-invites: - * post: - * summary: Generate and send invitation links to emails - * description: Generate invitation links for a list of emails and send them to the provided addresses. The invites are associated with the specified organization. - * tags: [Organization] - * parameters: - * - in: path - * name: org_id - * required: true - * schema: - * type: string - * description: The ID of the organization - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * email: - * type: array - * items: - * type: string - * description: The list of email addresses to send invitations to - * example: - * email: ["user1@example.com", "user2@example.com"] - * responses: - * 200: - * description: Invitations successfully sent. - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: Invitations successfully sent. - * 400: - * description: Invalid input data, email(s) are required. - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 400 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: Email(s) are required! - * 404: - * description: Organization not found. - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 404 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: Organization with ID {org_id} not found. - * 500: - * description: An unexpected error occurred while processing the request. - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 500 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: An unexpected error occurred - */ - - async generateAndSendInviteLinks( - req: Request, - res: Response, - next: NextFunction, - ) { - try { - const { email } = req.body; - const orgId = req.params.org_id; - - if (!email) { - throw new InvalidInput("Email(s) are required!"); - } - - const emailList = Array.isArray(email) ? email : [email]; - await this.orgService.generateAndSendInviteLinks(emailList, orgId); - res - .status(200) - .json({ status_code: 200, message: "Invitations successfully sent." }); - } catch (error) { - res.status(500).json({ - status_code: 500, - success: false, - message: error.message || "An unexpected error occurred", - }); - } - } - - /** - * @swagger - * /organizations/invites: - * get: - * summary: Get all invitation links - * description: Retrieve a paginated list of all invitation links. - * tags: [Organization] - * parameters: - * - in: query - * name: page - * schema: - * type: integer - * default: 1 - * description: The page number to retrieve - * - in: query - * name: pageSize - * schema: - * type: integer - * default: 10 - * description: The number of items per page - * responses: - * 200: - * description: Successfully fetched invites - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: Successfully fetched invites - * data: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * example: "123" - * token: - * type: string - * example: "abc123token" - * isAccepted: - * type: boolean - * example: false - * isGeneric: - * type: boolean - * example: false - * organization: - * type: string - * example: "Organization Name" - * email: - * type: string - * example: "user@example.com" - * total: - * type: integer - * example: 50 - * page: - * type: integer - * example: 1 - * pageSize: - * type: integer - * example: 10 - * 500: - * description: An unexpected error occurred while processing the request. - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 500 - * success: - * type: boolean - * example: false - * message: - * type: string - * example: An unexpected error occurred - */ - - async getAllInvite(req: Request, res: Response, next: NextFunction) { - try { - const page = parseInt(req.query.page as string, 10) || 1; - const pageSize = parseInt(req.query.pageSize as string, 10) || 10; - - const { status_code, message, data, total } = - await this.orgService.getAllInvite(page, pageSize); - - res - .status(200) - .json({ status_code, message, data, total, page, pageSize }); - } catch (error) { - res.status(500).json({ - status_code: 500, - success: false, - message: error.message || "An unexpected error occurred", - }); - } - } - - /** - * @swagger - * /api/v1/members/search: - * post: - * summary: Search organization members by name or email - * tags: [Members] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * example: "John Doe" - * email: - * type: string - * example: "johndoe@example.com" - * required: - * - name - * - email - * responses: - * 200: - * description: List of organization members matching the search criteria - * content: - * application/json: - * schema: - * type: object - * properties: - * result: - * type: array - * items: - * type: object - * properties: - * organizationId: - * type: string - * organizationName: - * type: string - * organizationEmail: - * type: string - * members: - * type: array - * items: - * type: object - * properties: - * userId: - * type: string - * userName: - * type: string - * userEmail: - * type: string - * status_code: - * type: integer - * example: 200 - * 400: - * description: At least one search criterion (name or email) must be provided - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * status_code: - * type: integer - * example: 400 - * 404: - * description: No members found matching the search criteria - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * status_code: - * type: integer - * example: 404 - * 500: - * description: An error occurred while searching for members - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * status_code: - * type: integer - * example: 500 - */ - async searchOrganizationMembers( - req: Request, - res: Response, - next: NextFunction, - ) { - const { name, email } = req.query; - - if (!name && !email) { - return res.status(400).json({ - error: - "At least one search criterion (name or email) must be provided.", - status_code: "400", - }); - } - - try { - const result = await this.orgService.searchOrganizationMembers({ - name: name as string, - email: email as string, - }); - if (result.length > 0) { - return res.status(200).json({ - result: result, - status_code: 200, - }); - } else { - return res.status(404).json({ - error: "No members found matching the search criteria.", - status_code: 404, - }); - } - } catch (error) { - next(error); - } - } - - /** - * @swagger - * /api/v1/organizations/{org_id}/roles/{role_id}: - * get: - * summary: Get a specific role in an organization - * tags: [Organizations] - * parameters: - * - in: path - * name: org_id - * schema: - * type: string - * required: true - * description: The ID of the organization - * - in: path - * name: role_id - * schema: - * type: string - * required: true - * description: The ID of the role - * responses: - * 200: - * description: The details of the specified role or a message indicating that the role does not exist - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 200 - * data: - * type: object - * properties: - * id: - * type: string - * example: "roleId123" - * name: - * type: string - * example: "Admin" - * description: - * type: string - * example: "Administrator role with full access" - * message: - * type: string - * example: "The role with ID roleId123 does not exist in the organisation" - * 400: - * description: Bad request, possibly due to invalid organization or role ID - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * status_code: - * type: integer - * example: 400 - * 401: - * description: Unauthorized, possibly due to missing or invalid credentials - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * status_code: - * type: integer - * example: 401 - * 404: - * description: Role or organization not found - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * status_code: - * type: integer - * example: 404 - * 500: - * description: An error occurred while fetching the role - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * status_code: - * type: integer - * example: 500 - */ - - async getSingleRole(req: Request, res: Response, next: NextFunction) { - try { - const organizationId = req.params.org_id; - const roleId = req.params.role_id; - const response = await this.orgService.fetchSingleRole( - organizationId, - roleId, - ); - - if (!response || response === null) { - return res.status(200).json({ - status_code: "200", - message: `The role with ID ${roleId} does not exist in the organisation`, - }); - } - - return res.status(200).json({ - status_code: 200, - data: response, - }); - } catch (error) { - if (error instanceof ResourceNotFound) { - next(error); - } - next(new ServerError("Encountered error while fetching user")); - } - } - - /** - * @swagger - * /api/v1/organizations/{org_id}/roles: - * post: - * summary: Create a new organization role - * tags: [Organization Roles] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: org_id - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateOrgRole' - * responses: - * 201: - * description: Organization role created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/OrganizationRole' - * 400: - * description: Bad request - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ErrorResponse' - * 404: - * description: Organization not found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ErrorResponse' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ErrorResponse' - * - * components: - * schemas: - * CreateOrgRole: - * type: object - * required: - * - name - * - description - * properties: - * name: - * type: string - * description: - * type: string - * OrganizationRole: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * description: - * type: string - * organization: - * $ref: '#/components/schemas/Organization' - * Organization: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * ErrorResponse: - * type: object - * properties: - * error: - * type: string - * status_code: - * type: integer - */ - async createOrganizationRole( - req: Request, - res: Response, - next: NextFunction, - ) { - try { - const organizationId = req.params.org_id; - const payload = req.body; - const response = await this.orgService.createOrganizationRole( - payload, - organizationId, - ); - - return res.status(201).json({ - status_code: 201, - data: response, - }); - } catch (err) { - if (err instanceof ResourceNotFound) { - next(err); - } - next(new ServerError("Error creating Organization roles")); - } - } - - async getAllOrganizationRoles( - req: Request, - res: Response, - next: NextFunction, - ) { - try { - const organizationId = req.params.org_id; - const response = - await this.orgService.fetchAllRolesInOrganization(organizationId); - - return res.status(200).json({ - status_code: 200, - data: response, - }); - } catch (error) { - if (error instanceof ResourceNotFound) { - next(error); - } - next(new ServerError("Error fetching all roles in organization")); - } - } - - /** - * @swagger - * /api/v1/organizations/{organizationId}/roles/{roleId}/permissions: - * put: - * summary: Update permissions for a specific role in an organization - * tags: [Roles] - * parameters: - * - in: path - * name: organizationId - * required: true - * description: The ID of the organization - * schema: - * type: string - * example: "org-12345" - * - in: path - * name: roleId - * required: true - * description: The ID of the role within the organization - * schema: - * type: string - * example: "role-67890" - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * permissions: - * type: array - * items: - * type: string - * enum: - * - canViewTransactions - * - canViewRefunds - * - canLogRefunds - * - canViewUsers - * - canCreateUsers - * - canEditUsers - * - canBlacklistWhitelistUsers - * example: - * - canViewTransactions - * - canCreateUsers - * - canLogRefunds - * required: - * - permissions - * responses: - * 200: - * description: Permissions updated successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status_code: - * type: integer - * example: 200 - * data: - * type: object - * description: The updated role object with permissions - * 400: - * description: Bad Request - Missing required parameters or permissions - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "OrganizationID and Role ID are required." - * status_code: - * type: integer - * example: 400 - * 404: - * description: Not Found - Organization or Role not found - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Role not found" - * status_code: - * type: integer - * example: 404 - * 500: - * description: Internal Server Error - Error updating permissions - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Error updating the role permissions of this organization" - * status_code: - * type: integer - * example: 500 - */ - - async updateOrganizationRolePermissions( - req: Request, - res: Response, - next: NextFunction, - ) { - try { - const organizationId = req?.params?.org_id || null; - const roleId = req?.params?.role_id || null; - const newPermissions: PermissionCategory[] = req.body?.permissions || []; - - if (!(organizationId && roleId)) { - return res.status(400).json({ - error: "OrganizationID and Role ID are required.", - status_code: 400, - }); - } - - if (!newPermissions?.length) { - return res.status(400).json({ - error: "Permissions are required.", - status_code: 400, - }); - } - - const response = await this.orgService.updateRolePermissions( - roleId, - organizationId, - newPermissions, - ); - - return res.status(200).json({ - status_code: 200, - data: response, - }); - } catch (error) { - if (error instanceof ResourceNotFound) { - next(error); - } - next( - new ServerError( - "Error updating the role permissions of this organization", - ), - ); - } - } -} +import { NextFunction, Request, Response } from "express"; +import { PermissionCategory } from "../enums/permission-category.enum"; +import { + HttpError, + InvalidInput, + ResourceNotFound, + ServerError, +} from "../middleware"; +import { OrgService } from "../services/org.services"; +import log from "../utils/logger"; + +export class OrgController { + private orgService: OrgService; + constructor() { + this.orgService = new OrgService(); + } + + /** + * @swagger + * /api/v1/organizations: + * post: + * summary: Create a new organisation + * description: This endpoint allows a user to create a new organisation + * tags: [Organisation] + * operationId: createOrganisation + * security: + * - bearerAuth: [] + * requestBody: + * description: Organisation payload + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * example: My Organisation + * description: + * type: string + * example: This is a sample organisation. + * email: + * type: string + * example: name@gmail.com + * industry: + * type: string + * example: entertainment + * type: + * type: string + * example: music + * country: + * type: string + * example: Nigeria + * address: + * type: string + * example: 121 ikeja + * state: + * type: string + * example: Oyo + * required: + * - name + * - description + * - email + * - industry + * - type + * - country + * - address + * - state + * responses: + * '201': + * description: Organisation created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * message: + * type: string + * example: Organisation created successfully + * data: + * type: object + * properties: + * id: + * type: string + * example: "1" + * name: + * type: string + * example: My Organisation + * description: + * type: string + * example: This is a sample organisation. + * email: + * type: string + * example: abc@gmail.com + * industry: + * type: string + * example: entertainment + * type: + * type: string + * example: music + * country: + * type: string + * example: Nigeria + * address: + * type: string + * example: 121 ikeja + * state: + * type: string + * example: Oyo + * slug: + * type: string + * example: 86820688-fd94-4b58-9bdd-99a701714a77 + * owner_id: + * type: string + * example: 86820688-fd94-4b58-9bdd-99a701714a76 + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * status_code: + * type: integer + * example: 201 + * '400': + * description: Bad Request + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Invalid input + * status_code: + * type: integer + * example: 400 + * '500': + * description: Internal Server Error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * message: + * type: string + * example: Internal server error + * status_code: + * type: integer + * example: 500 + * components: + * securitySchemes: + * bearerAuth: + * type: http + * scheme: bearer + * bearerFormat: JWT + */ + + async createOrganisation(req: Request, res: Response, next: NextFunction) { + try { + const payload = req.body; + const user = req.user; + const userId = user.id; + + const organisationService = new OrgService(); + const new_organisation = await organisationService.createOrganisation( + payload, + userId, + ); + + const respObj = { + status: "success", + message: "organisation created successfully", + data: new_organisation, + status_code: 201, + }; + + return res.status(201).json(respObj); + } catch (error) { + next(error); + } + } + + /** + * @swagger + * /api/v1/users/{userId}/organizations: + * get: + * summary: Get user organizations + * description: Retrieve all organizations associated with a specific user + * tags: [Organisation] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: string + * description: The ID of the user + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Organizations retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: Organizations retrieved successfully. + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * 400: + * description: Invalid user ID or authentication mismatch + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 400 + * message: + * type: string + * example: Invalid user ID or authentication mismatch. + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 500 + * message: + * type: string + * example: Failed to retrieve organizations. Please try again later. + */ + + async getOrganizations(req: Request, res: Response) { + try { + const userId = req.params.id; + log.info("req.user:", req.user); + if (!req.user || req.user.id !== userId) { + return res.status(400).json({ + status: "unsuccessful", + status_code: 400, + message: "Invalid user ID or authentication mismatch.", + }); + } + const organizations = + await this.orgService.getOrganizationsByUserId(userId); + + if (organizations.length === 0) { + return res.status(200).json({ + status: "success", + status_code: 200, + message: "No organizations found for this user.", + data: [], + }); + } + + res.status(200).json({ + status: "success", + status_code: 200, + message: "Organizations retrieved successfully.", + data: organizations, + }); + } catch (error) { + log.error("Failed to retrieve organizations:", error); + res.status(500).json({ + status: "unsuccessful", + status_code: 500, + message: "Failed to retrieve organizations. Please try again later.", + }); + } + } + + /** + * @swagger + * /api/v1/organizations/{org_id}: + * get: + * summary: Get a single organization + * description: Retrieve details of a specific organization by its ID + * tags: [Organisation] + * parameters: + * - in: path + * name: org_id + * required: true + * schema: + * type: string + * description: The ID of the organization + * responses: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * status_code: + * type: integer + * example: 200 + * data: + * type: object + * properties: + * org_id: + * type: string + * example: "2928a3d6-2b85-4abc-9438-ff9769b126ed" + * name: + * type: string + * example: "Organisation 1" + * description: + * type: string + * example: "Description of the organisation" + * 404: + * description: Organization not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: forbidden + * message: + * type: string + * example: Organization not found + * status_code: + * type: integer + * example: 404 + * 500: + * description: Server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 500 + * message: + * type: string + * example: Failed to get user organisation. Please try again later. + */ + + async getSingleOrg(req: Request, res: Response) { + try { + const org = await this.orgService.getSingleOrg( + req.params.org_id, + req.user.id, + ); + if (!org) { + return res.status(404).json({ + status: "forbidden", + message: "Organization not found", + status_code: 404, + }); + } + + res.status(200).json({ + status: "success", + status_code: 200, + data: { + org_id: org.id, + name: org.name, + description: org.description, + }, + }); + } catch (error) { + res.status(500).json({ + status: "unsuccessful", + status_code: 500, + message: "Failed to get user organisation. Please try again later.", + }); + } + } + + /** + * @swagger + * /api/v1/organizations/{org_id}/user/{user_id}: + * delete: + * summary: Remove a user from an organization + * description: Delete a user from a specific organization by user ID and organization ID + * tags: [Organisation] + * parameters: + * - in: path + * name: org_id + * required: true + * schema: + * type: string + * description: The ID of the organization + * - in: path + * name: user_id + * required: true + * schema: + * type: string + * description: The ID of the user + * responses: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: User deleted successfully + * 404: + * description: User not found in the organization + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: forbidden + * message: + * type: string + * example: User not found in the organization + * status_code: + * type: integer + * example: 404 + * 400: + * description: Failed to remove user from organization + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: Bad Request + * message: + * type: string + * example: Failed to remove user from organization + * status_code: + * type: integer + * example: 400 + */ + async removeUser(req: Request, res: Response) { + try { + const user = await this.orgService.removeUser( + req.params.org_id, + req.params.user_id, + ); + + if (!user) { + return res.status(404).json({ + status: "forbidden", + message: "User not found in the organization", + status_code: 404, + }); + } + res.status(200).json({ + status: "success", + message: "User deleted successfully", + status_code: 200, + }); + } catch (error) { + res.status(400).json({ + status: "Bad Request", + message: "Failed to remove user from organization", + status_code: "400", + }); + } + } + + /** + * @swagger + * /organizations/{organization_id}: + * put: + * summary: Update organization details + * description: Update the details of an existing organization + * parameters: + * - in: path + * name: organization_id + * required: true + * schema: + * type: string + * description: The ID of the organization to update + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - email + * - industry + * - type + * - country + * - address + * - state + * - description + * properties: + * name: + * type: string + * example: "New Organization Name" + * email: + * type: string + * example: "newemail@example.com" + * industry: + * type: string + * example: "Tech" + * type: + * type: string + * example: "Private" + * country: + * type: string + * example: "NGA" + * address: + * type: string + * example: "1234 New HNG" + * state: + * type: string + * example: "Lagos" + * description: + * type: string + * example: "A new description of the organization." + * responses: + * 200: + * description: Organization updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: "Organisation updated successfully" + * data: + * type: object + * properties: + * organization_id: + * type: string + * example: "61202249-0bc4-41eb-8cd5-7b873b7c7cc7" + * name: + * type: string + * example: "New Organization Name" + * email: + * type: string + * example: "newemail@example.com" + * industry: + * type: string + * example: "Tech" + * type: + * type: string + * example: "Private" + * country: + * type: string + * example: "NGA" + * address: + * type: string + * example: "1234 New HNG" + * state: + * type: string + * example: "Lagos" + * description: + * type: string + * example: "A new description of the organization." + * 404: + * description: Organization not found + * 500: + * description: Failed to update organization details + */ + + async updateOrganisation(req: Request, res: Response, next: NextFunction) { + try { + const orgId = req.params.organization_id; + const payload = req.body; + const userId = req.user.id; + + const updatedOrganisation = + await this.orgService.updateOrganizationDetails(orgId, userId, payload); + + const { + id, + name, + email, + industry, + type, + country, + address, + state, + description, + } = updatedOrganisation; + + const respObj = { + status: "success", + status_code: 200, + message: "Organisation updated successfully", + data: { + organization_id: id, + name, + email, + industry, + type, + country, + address, + state, + description, + }, + }; + + return res.status(200).json(respObj); + } catch (error) { + if (error instanceof ResourceNotFound) { + next(error); + } else { + next(new HttpError(500, "Failed to update organization details")); + } + } + } + + /** + * @swagger + * /organizations/accept-invite: + * post: + * summary: Add user to organization using an invite token + * description: Adds a user to an organization using an invite token. The user must be registered to join the organization. + * tags: [Organization] + * parameters: + * - in: query + * name: token + * required: true + * schema: + * type: string + * description: The invitation token + * responses: + * 201: + * description: User added to organization successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 201 + * message: + * type: string + * example: User added to organization successfully + * 404: + * description: Invalid or expired invite token, or user not registered. + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 404 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Invalid or expired invite token + * 409: + * description: User already added to organization. + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 409 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: User already added to organization + * 500: + * description: An unexpected error occurred while processing the request. + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 500 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: An unexpected error occurred + */ + + async addUserToOrganizationWithInvite( + req: Request, + res: Response, + next: NextFunction, + ) { + try { + const { token } = req.query; + const userId = req.user.id; + const message = await this.orgService.addUserToOrganizationWithInvite( + token as string, + userId, + ); + + res.status(201).json({ status_code: 201, message: message }); + } catch (error) { + res.status(500).json({ + status_code: 500, + success: false, + message: error.message || "An unexpected error occurred", + }); + } + } + + /** + * @swagger + * /organizations/{org_id}/invite: + * get: + * summary: Generate a generic invite link for an organization + * description: Generate a generic invite link that can be used to invite users to join the specified organization. The invite link is returned for sharing or use in invitation emails. + * tags: [Organization] + * parameters: + * - in: path + * name: org_id + * required: true + * schema: + * type: string + * description: The ID of the organization + * responses: + * 200: + * description: Generic invite link generated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: Invite link generated successfully + * link: + * type: string + * example: "http://example.com/invite?token=abc123" + * 404: + * description: Organization not found. + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 404 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Organization with ID {org_id} not found + * 500: + * description: An unexpected error occurred while processing the request. + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 500 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: An unexpected error occurred + */ + + async generateGenericInviteLink( + req: Request, + res: Response, + next: NextFunction, + ) { + try { + const link = await this.orgService.generateGenericInviteLink( + req.params.org_id, + ); + if (link) { + res.status(200).json({ + status_code: 200, + message: "Invite link generated successfully", + link, + }); + } + } catch (error) { + res.status(500).json({ + status_code: 500, + success: false, + message: error.message || "An unexpected error occurred", + }); + } + } + + /** + * @swagger + * /organizations/{org_id}/send-invites: + * post: + * summary: Generate and send invitation links to emails + * description: Generate invitation links for a list of emails and send them to the provided addresses. The invites are associated with the specified organization. + * tags: [Organization] + * parameters: + * - in: path + * name: org_id + * required: true + * schema: + * type: string + * description: The ID of the organization + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: array + * items: + * type: string + * description: The list of email addresses to send invitations to + * example: + * email: ["user1@example.com", "user2@example.com"] + * responses: + * 200: + * description: Invitations successfully sent. + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: Invitations successfully sent. + * 400: + * description: Invalid input data, email(s) are required. + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 400 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Email(s) are required! + * 404: + * description: Organization not found. + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 404 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: Organization with ID {org_id} not found. + * 500: + * description: An unexpected error occurred while processing the request. + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 500 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: An unexpected error occurred + */ + + async generateAndSendInviteLinks( + req: Request, + res: Response, + next: NextFunction, + ) { + try { + const { email } = req.body; + const orgId = req.params.org_id; + + if (!email) { + throw new InvalidInput("Email(s) are required!"); + } + + const emailList = Array.isArray(email) ? email : [email]; + await this.orgService.generateAndSendInviteLinks(emailList, orgId); + res + .status(200) + .json({ status_code: 200, message: "Invitations successfully sent." }); + } catch (error) { + res.status(500).json({ + status_code: 500, + success: false, + message: error.message || "An unexpected error occurred", + }); + } + } + + /** + * @swagger + * /organizations/invites: + * get: + * summary: Get all invitation links + * description: Retrieve a paginated list of all invitation links. + * tags: [Organization] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: The page number to retrieve + * - in: query + * name: pageSize + * schema: + * type: integer + * default: 10 + * description: The number of items per page + * responses: + * 200: + * description: Successfully fetched invites + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: Successfully fetched invites + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * example: "123" + * token: + * type: string + * example: "abc123token" + * isAccepted: + * type: boolean + * example: false + * isGeneric: + * type: boolean + * example: false + * organization: + * type: string + * example: "Organization Name" + * email: + * type: string + * example: "user@example.com" + * total: + * type: integer + * example: 50 + * page: + * type: integer + * example: 1 + * pageSize: + * type: integer + * example: 10 + * 500: + * description: An unexpected error occurred while processing the request. + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 500 + * success: + * type: boolean + * example: false + * message: + * type: string + * example: An unexpected error occurred + */ + + async getAllInvite(req: Request, res: Response, next: NextFunction) { + try { + const page = parseInt(req.query.page as string, 10) || 1; + const pageSize = parseInt(req.query.pageSize as string, 10) || 10; + + const { status_code, message, data, total } = + await this.orgService.getAllInvite(page, pageSize); + + res + .status(200) + .json({ status_code, message, data, total, page, pageSize }); + } catch (error) { + res.status(500).json({ + status_code: 500, + success: false, + message: error.message || "An unexpected error occurred", + }); + } + } + + /** + * @swagger + * /api/v1/members/search: + * post: + * summary: Search organization members by name or email + * tags: [Members] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * example: "John Doe" + * email: + * type: string + * example: "johndoe@example.com" + * required: + * - name + * - email + * responses: + * 200: + * description: List of organization members matching the search criteria + * content: + * application/json: + * schema: + * type: object + * properties: + * result: + * type: array + * items: + * type: object + * properties: + * organizationId: + * type: string + * organizationName: + * type: string + * organizationEmail: + * type: string + * members: + * type: array + * items: + * type: object + * properties: + * userId: + * type: string + * userName: + * type: string + * userEmail: + * type: string + * status_code: + * type: integer + * example: 200 + * 400: + * description: At least one search criterion (name or email) must be provided + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * status_code: + * type: integer + * example: 400 + * 404: + * description: No members found matching the search criteria + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * status_code: + * type: integer + * example: 404 + * 500: + * description: An error occurred while searching for members + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * status_code: + * type: integer + * example: 500 + */ + async searchOrganizationMembers( + req: Request, + res: Response, + next: NextFunction, + ) { + const { name, email } = req.query; + + if (!name && !email) { + return res.status(400).json({ + error: + "At least one search criterion (name or email) must be provided.", + status_code: "400", + }); + } + + try { + const result = await this.orgService.searchOrganizationMembers({ + name: name as string, + email: email as string, + }); + if (result.length > 0) { + return res.status(200).json({ + result: result, + status_code: 200, + }); + } else { + return res.status(404).json({ + error: "No members found matching the search criteria.", + status_code: 404, + }); + } + } catch (error) { + next(error); + } + } + + /** + * @swagger + * /api/v1/organizations/{org_id}/roles/{role_id}: + * get: + * summary: Get a specific role in an organization + * tags: [Organizations] + * parameters: + * - in: path + * name: org_id + * schema: + * type: string + * required: true + * description: The ID of the organization + * - in: path + * name: role_id + * schema: + * type: string + * required: true + * description: The ID of the role + * responses: + * 200: + * description: The details of the specified role or a message indicating that the role does not exist + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 200 + * data: + * type: object + * properties: + * id: + * type: string + * example: "roleId123" + * name: + * type: string + * example: "Admin" + * description: + * type: string + * example: "Administrator role with full access" + * message: + * type: string + * example: "The role with ID roleId123 does not exist in the organisation" + * 400: + * description: Bad request, possibly due to invalid organization or role ID + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * status_code: + * type: integer + * example: 400 + * 401: + * description: Unauthorized, possibly due to missing or invalid credentials + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * status_code: + * type: integer + * example: 401 + * 404: + * description: Role or organization not found + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * status_code: + * type: integer + * example: 404 + * 500: + * description: An error occurred while fetching the role + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * status_code: + * type: integer + * example: 500 + */ + + async getSingleRole(req: Request, res: Response, next: NextFunction) { + try { + const organizationId = req.params.org_id; + const roleId = req.params.role_id; + const response = await this.orgService.fetchSingleRole( + organizationId, + roleId, + ); + + if (!response || response === null) { + return res.status(200).json({ + status_code: "200", + message: `The role with ID ${roleId} does not exist in the organisation`, + }); + } + + return res.status(200).json({ + status_code: 200, + data: response, + }); + } catch (error) { + if (error instanceof ResourceNotFound) { + next(error); + } + next(new ServerError("Encountered error while fetching user")); + } + } + + /** + * @swagger + * /api/v1/organizations/{org_id}/roles: + * post: + * summary: Create a new organization role + * tags: [Organization Roles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: org_id + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CreateOrgRole' + * responses: + * 201: + * description: Organization role created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrganizationRole' + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * 404: + * description: Organization not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * + * components: + * schemas: + * CreateOrgRole: + * type: object + * required: + * - name + * - description + * properties: + * name: + * type: string + * description: + * type: string + * OrganizationRole: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * description: + * type: string + * organization: + * $ref: '#/components/schemas/Organization' + * Organization: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * ErrorResponse: + * type: object + * properties: + * error: + * type: string + * status_code: + * type: integer + */ + async createOrganizationRole( + req: Request, + res: Response, + next: NextFunction, + ) { + try { + const organizationId = req.params.org_id; + const payload = req.body; + const response = await this.orgService.createOrganizationRole( + payload, + organizationId, + ); + + return res.status(201).json({ + status_code: 201, + data: response, + }); + } catch (err) { + if (err instanceof ResourceNotFound) { + next(err); + } + next(new ServerError("Error creating Organization roles")); + } + } + + async getAllOrganizationRoles( + req: Request, + res: Response, + next: NextFunction, + ) { + try { + const organizationId = req.params.org_id; + const response = + await this.orgService.fetchAllRolesInOrganization(organizationId); + + return res.status(200).json({ + status_code: 200, + data: response, + }); + } catch (error) { + if (error instanceof ResourceNotFound) { + next(error); + } + next(new ServerError("Error fetching all roles in organization")); + } + } + + /** + * @swagger + * /api/v1/organizations/{organizationId}/roles/{roleId}/permissions: + * put: + * summary: Update permissions for a specific role in an organization + * tags: [Roles] + * parameters: + * - in: path + * name: organizationId + * required: true + * description: The ID of the organization + * schema: + * type: string + * example: "org-12345" + * - in: path + * name: roleId + * required: true + * description: The ID of the role within the organization + * schema: + * type: string + * example: "role-67890" + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * permissions: + * type: array + * items: + * type: string + * enum: + * - canViewTransactions + * - canViewRefunds + * - canLogRefunds + * - canViewUsers + * - canCreateUsers + * - canEditUsers + * - canBlacklistWhitelistUsers + * example: + * - canViewTransactions + * - canCreateUsers + * - canLogRefunds + * required: + * - permissions + * responses: + * 200: + * description: Permissions updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 200 + * data: + * type: object + * description: The updated role object with permissions + * 400: + * description: Bad Request - Missing required parameters or permissions + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "OrganizationID and Role ID are required." + * status_code: + * type: integer + * example: 400 + * 404: + * description: Not Found - Organization or Role not found + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Role not found" + * status_code: + * type: integer + * example: 404 + * 500: + * description: Internal Server Error - Error updating permissions + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Error updating the role permissions of this organization" + * status_code: + * type: integer + * example: 500 + */ + + async updateOrganizationRolePermissions( + req: Request, + res: Response, + next: NextFunction, + ) { + try { + const organizationId = req?.params?.org_id || null; + const roleId = req?.params?.role_id || null; + const newPermissions: PermissionCategory[] = req.body?.permissions || []; + + if (!(organizationId && roleId)) { + return res.status(400).json({ + error: "OrganizationID and Role ID are required.", + status_code: 400, + }); + } + + if (!newPermissions?.length) { + return res.status(400).json({ + error: "Permissions are required.", + status_code: 400, + }); + } + + const response = await this.orgService.updateRolePermissions( + roleId, + organizationId, + newPermissions, + ); + + return res.status(200).json({ + status_code: 200, + data: response, + }); + } catch (error) { + if (error instanceof ResourceNotFound) { + next(error); + } + next( + new ServerError( + "Error updating the role permissions of this organization", + ), + ); + } + } +} diff --git a/src/controllers/PaymentController.ts b/src/controllers/PaymentController.ts index faadfd86..389c9af2 100644 --- a/src/controllers/PaymentController.ts +++ b/src/controllers/PaymentController.ts @@ -1,152 +1,152 @@ -import { Request, Response } from "express"; -import { initializePayment, verifyPayment } from "../services"; -import { Payment } from "../models"; -import dataSource from "../data-source"; - -export class PaymentController { - static async initiatePayment(req: Request, res: Response): Promise { - try { - const userId = req.user.id; - const paymentDetails = req.body; - const paymentResponse = await initializePayment({ - ...paymentDetails, - userId, - }); - - return res.json(paymentResponse); - } catch (error) { - return res.status(500).json({ error: "Payment initiation failed" }); - } - } - - static async verifyPayment(req: Request, res: Response): Promise { - try { - const { transactionId } = req.params; - const verificationResponse = await verifyPayment(transactionId); - - // const paymentRepository = dataSource.getRepository(Payment); - // const payment = await paymentRepository.findOneBy({ id: transactionId }); - - // if (payment) { - // // payment.status = verificationResponse.status === 'successful' ? 'completed' : 'failed'; - // await paymentRepository.save(payment); - // } - - return res.json(verificationResponse); - } catch (error) { - return res.status(500).json({ error: "Payment verification failed" }); - } - } -} - -/** - * @swagger - * tags: - * name: Payments - * description: Payment related operations - */ - -/** - * @swagger - * /api/v1/payments/flutterwave/initiate: - * post: - * summary: Initiate a payment with Flutterwave - * tags: [Payments] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - card_number - * - cvv - * - expiry_month - * - expiry_year - * - email - * - fullname - * - phone_number - * - currency - * - amount - * - payer_id - * - payer_type - * properties: - * card_number: - * type: string - * cvv: - * type: string - * expiry_month: - * type: string - * expiry_year: - * type: string - * email: - * type: string - * fullname: - * type: string - * phone_number: - * type: string - * currency: - * type: string - * amount: - * type: number - * payer_id: - * type: string - * payer_type: - * type: string - * enum: [user, organization] - * responses: - * 200: - * description: Payment initiated successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * message: - * type: string - * data: - * type: object - * 401: - * description: Unauthorized - * 500: - * description: Payment initiation failed - */ - -/** - * @swagger - * /api/v1/payments/flutterwave/verify/{transactionId}: - * get: - * summary: Verify a Flutterwave payment - * tags: [Payments] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: transactionId - * required: true - * schema: - * type: string - * description: The ID of the transaction to verify - * responses: - * 200: - * description: Payment verified successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * message: - * type: string - * data: - * type: object - * 401: - * description: Unauthorized - * 500: - * description: Payment verification failed - */ +import { Request, Response } from "express"; +import { initializePayment, verifyPayment } from "../services"; +import { Payment } from "../models"; +import dataSource from "../data-source"; + +export class PaymentController { + static async initiatePayment(req: Request, res: Response): Promise { + try { + const userId = req.user.id; + const paymentDetails = req.body; + const paymentResponse = await initializePayment({ + ...paymentDetails, + userId, + }); + + return res.json(paymentResponse); + } catch (error) { + return res.status(500).json({ error: "Payment initiation failed" }); + } + } + + static async verifyPayment(req: Request, res: Response): Promise { + try { + const { transactionId } = req.params; + const verificationResponse = await verifyPayment(transactionId); + + // const paymentRepository = dataSource.getRepository(Payment); + // const payment = await paymentRepository.findOneBy({ id: transactionId }); + + // if (payment) { + // // payment.status = verificationResponse.status === 'successful' ? 'completed' : 'failed'; + // await paymentRepository.save(payment); + // } + + return res.json(verificationResponse); + } catch (error) { + return res.status(500).json({ error: "Payment verification failed" }); + } + } +} + +/** + * @swagger + * tags: + * name: Payments + * description: Payment related operations + */ + +/** + * @swagger + * /api/v1/payments/flutterwave/initiate: + * post: + * summary: Initiate a payment with Flutterwave + * tags: [Payments] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - card_number + * - cvv + * - expiry_month + * - expiry_year + * - email + * - fullname + * - phone_number + * - currency + * - amount + * - payer_id + * - payer_type + * properties: + * card_number: + * type: string + * cvv: + * type: string + * expiry_month: + * type: string + * expiry_year: + * type: string + * email: + * type: string + * fullname: + * type: string + * phone_number: + * type: string + * currency: + * type: string + * amount: + * type: number + * payer_id: + * type: string + * payer_type: + * type: string + * enum: [user, organization] + * responses: + * 200: + * description: Payment initiated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * message: + * type: string + * data: + * type: object + * 401: + * description: Unauthorized + * 500: + * description: Payment initiation failed + */ + +/** + * @swagger + * /api/v1/payments/flutterwave/verify/{transactionId}: + * get: + * summary: Verify a Flutterwave payment + * tags: [Payments] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: transactionId + * required: true + * schema: + * type: string + * description: The ID of the transaction to verify + * responses: + * 200: + * description: Payment verified successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * message: + * type: string + * data: + * type: object + * 401: + * description: Unauthorized + * 500: + * description: Payment verification failed + */ diff --git a/src/controllers/PaymentLemonSqueezyController.ts b/src/controllers/PaymentLemonSqueezyController.ts index 6d0c174d..8dc15a24 100644 --- a/src/controllers/PaymentLemonSqueezyController.ts +++ b/src/controllers/PaymentLemonSqueezyController.ts @@ -1,129 +1,129 @@ -/** - * @swagger - * tags: - * name: Payments - * description: Payment management with LemonSqueezy - */ - -import { Request, Response } from "express"; -import crypto from "crypto"; -import config from "../config"; -import { Payment } from "../models/payment"; -import AppDataSource from "../data-source"; - -/** - * @swagger - * /api/v1/payments/lemonsqueezy/initiate: - * get: - * summary: Initiates a payment using LemonSqueezy - * tags: [Payments] - * responses: - * 200: - * description: Payment initiation link - * content: - * text/html: - * schema: - * type: string - * example: Make Payments - * 500: - * description: An error occurred while processing the payment - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: An error occurred while processing the payment - */ - -export const makePaymentLemonSqueezy = async (req: Request, res: Response) => { - try { - return res.send( - `Make Payments`, - ); - } catch (error) { - res - .status(500) - .json({ error: "An error occurred while processing the payment" }); - } -}; - -/** - * @swagger - * /api/v1/payments/lemonsqueezy/webhook: - * post: - * summary: Handles LemonSqueezy webhook notifications - * tags: [Payments] - * requestBody: - * required: true - * content: - * text/plain: - * schema: - * type: string - * responses: - * 200: - * description: Webhook received successfully - * content: - * text/plain: - * schema: - * type: string - * example: Webhook received - * 400: - * description: Webhook verification failed - * content: - * text/plain: - * schema: - * type: string - * example: Webhook verification failed - */ - -export const LemonSqueezyWebhook = async (req: Request, res: Response) => { - try { - const secret = config.LEMONSQUEEZY_SIGNING_KEY; - const rawBody = req.body; - if (!rawBody) { - throw new Error("No body"); - } - - //verify the key signature sent to the webhook - const signature = req.get("X-Signature"); - const hmac = crypto.createHmac("sha256", secret); - hmac.update(rawBody); - const digest = hmac.digest("hex"); - - if ( - !signature || - !crypto.timingSafeEqual( - Buffer.from(digest, "hex"), - Buffer.from(signature, "hex"), - ) - ) { - throw new Error("Invalid signature."); - } - - const data = JSON.parse(rawBody); - const { subtotal, currency, status, user_email, created_at, updated_at } = - data.data.attributes; - - const amount = subtotal; - const payer_email = user_email; - const formattedAmount = parseFloat(parseFloat(amount).toFixed(2)); - const mappedStatus = status === "paid" ? "completed" : status; - - const paymentRepository = AppDataSource.getRepository(Payment); - const payment = paymentRepository.create({ - amount: formattedAmount, - currency, - status: mappedStatus, - provider: "lemonsqueezy", - created_at, - updated_at, - }); - - await paymentRepository.save(payment); - res.status(200).send("Webhook received"); - } catch (error) { - res.status(400).send("Webhook verification failed"); - } -}; +/** + * @swagger + * tags: + * name: Payments + * description: Payment management with LemonSqueezy + */ + +import { Request, Response } from "express"; +import crypto from "crypto"; +import config from "../config"; +import { Payment } from "../models/payment"; +import AppDataSource from "../data-source"; + +/** + * @swagger + * /api/v1/payments/lemonsqueezy/initiate: + * get: + * summary: Initiates a payment using LemonSqueezy + * tags: [Payments] + * responses: + * 200: + * description: Payment initiation link + * content: + * text/html: + * schema: + * type: string + * example: Make Payments + * 500: + * description: An error occurred while processing the payment + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: An error occurred while processing the payment + */ + +export const makePaymentLemonSqueezy = async (req: Request, res: Response) => { + try { + return res.send( + `Make Payments`, + ); + } catch (error) { + res + .status(500) + .json({ error: "An error occurred while processing the payment" }); + } +}; + +/** + * @swagger + * /api/v1/payments/lemonsqueezy/webhook: + * post: + * summary: Handles LemonSqueezy webhook notifications + * tags: [Payments] + * requestBody: + * required: true + * content: + * text/plain: + * schema: + * type: string + * responses: + * 200: + * description: Webhook received successfully + * content: + * text/plain: + * schema: + * type: string + * example: Webhook received + * 400: + * description: Webhook verification failed + * content: + * text/plain: + * schema: + * type: string + * example: Webhook verification failed + */ + +export const LemonSqueezyWebhook = async (req: Request, res: Response) => { + try { + const secret = config.LEMONSQUEEZY_SIGNING_KEY; + const rawBody = req.body; + if (!rawBody) { + throw new Error("No body"); + } + + //verify the key signature sent to the webhook + const signature = req.get("X-Signature"); + const hmac = crypto.createHmac("sha256", secret); + hmac.update(rawBody); + const digest = hmac.digest("hex"); + + if ( + !signature || + !crypto.timingSafeEqual( + Buffer.from(digest, "hex"), + Buffer.from(signature, "hex"), + ) + ) { + throw new Error("Invalid signature."); + } + + const data = JSON.parse(rawBody); + const { subtotal, currency, status, user_email, created_at, updated_at } = + data.data.attributes; + + const amount = subtotal; + const payer_email = user_email; + const formattedAmount = parseFloat(parseFloat(amount).toFixed(2)); + const mappedStatus = status === "paid" ? "completed" : status; + + const paymentRepository = AppDataSource.getRepository(Payment); + const payment = paymentRepository.create({ + amount: formattedAmount, + currency, + status: mappedStatus, + provider: "lemonsqueezy", + created_at, + updated_at, + }); + + await paymentRepository.save(payment); + res.status(200).send("Webhook received"); + } catch (error) { + res.status(400).send("Webhook verification failed"); + } +}; diff --git a/src/controllers/ProductController.ts b/src/controllers/ProductController.ts index 24d534b1..37a3dfd1 100644 --- a/src/controllers/ProductController.ts +++ b/src/controllers/ProductController.ts @@ -18,10 +18,12 @@ class ProductController { /** * @openapi - * /api/v1/organizations/{org_id}/product: + * /api/v1/organisation/{:id}/product: * post: - * summary: Create a product - * tags: [Product] + * tags: + * - Product API + * summary: Create a new product + * description: Create a new product for organisations. * parameters: * - name: org_id * in: path @@ -252,7 +254,7 @@ class ProductController { /** * @openapi - * /api/v1/organizations/{org_id}/products/{product_id}: + * /api/v1/products/{product_id}: * delete: * summary: Delete a product by its ID * tags: [Product] @@ -333,6 +335,187 @@ class ProductController { res.status(200).json({ message: "Product deleted successfully" }); }; + /** + * @swagger + * /api/v1/{org_id}/products/{product_id}: + * put: + * summary: Update a product + * description: Update details of a product within an organization. + * tags: + * - Products + * parameters: + * - in: path + * name: org_id + * required: true + * schema: + * type: string + * description: The ID of the organization + * - in: path + * name: product_id + * required: true + * schema: + * type: string + * description: The ID of the product + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * description: + * type: string + * example: "product 1 updated price" + * price: + * type: number + * example: 30 + * name: + * type: string + * category: + * type: string + * quantity: + * type: integer + * image: + * type: string + * size: + * type: string + * stock_status: + * type: string + * responses: + * 200: + * description: Product updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: "product update successful" + * data: + * type: object + * properties: + * id: + * type: string + * example: "b14e4406-80f6-4029-9681-715798741c8f" + * name: + * type: string + * example: "Product 1" + * description: + * type: string + * example: "product 1 updated price" + * price: + * type: number + * example: 30 + * quantity: + * type: integer + * example: 1 + * category: + * type: string + * example: "test product" + * image: + * type: string + * example: "image url" + * updated_at: + * type: string + * format: date-time + * example: "2024-08-08T09:06:36.210Z" + * created_at: + * type: string + * format: date-time + * example: "2024-08-08T07:12:33.936Z" + * size: + * type: string + * example: "Standard" + * stock_status: + * type: string + * example: "low on stock" + * 400: + * description: Bad request + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 400 + * message: + * type: string + * example: "user not a member of organization" + * 403: + * description: Forbidden + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 403 + * message: + * type: string + * example: "Forbidden: User not an admin or super admin" + * 404: + * description: Resource not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 404 + * message: + * type: string + * example: "Product with id b14e4406-80f6-4029-9681-715798741c8f not found" + * 422: + * description: Unprocessable Entity + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 422 + * message: + * type: string + * example: "Product ID not provided" + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status_code: + * type: integer + * example: 500 + * message: + * type: string + * example: "Internal server error" + */ + public updateProduct = async (req: Request, res: Response) => { + const { org_id, product_id } = req.params; + const updatedProduct = await this.productService.updateProduct( + org_id, + product_id, + req.body, + ); + res.status(200).json({ + status_code: 200, + message: "product update successful", + data: updatedProduct, + }); + }; + /** * @openapi * /api/v1/organizations/{org_id}/products/{product_id}: @@ -436,7 +619,6 @@ class ProductController { * type: integer * example: 500 */ - public getSingleProduct = async (req: Request, res: Response) => { const { org_id, product_id } = req.params; if (product_id && org_id) { diff --git a/src/controllers/SmsController.ts b/src/controllers/SmsController.ts index 962bc771..5832814b 100644 --- a/src/controllers/SmsController.ts +++ b/src/controllers/SmsController.ts @@ -1,187 +1,187 @@ -/** - * @swagger - * /api/v1/sms/send: - * post: - * summary: Send an SMS - * description: Sends an SMS message to the specified phone number. - * tags: [SMS] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - phone_number - * - message - * properties: - * phone_number: - * type: string - * example: "+23465544466" - * description: The phone number to send the SMS to. - * message: - * type: string - * example: "Hello, this is a test message." - * description: The content of the SMS message. - * responses: - * 200: - * description: SMS added to the queue successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: success - * status_code: - * type: integer - * example: 200 - * message: - * type: string - * example: SMS added to the queue successfully. - * 400: - * description: Invalid input data. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 400 - * message: - * type: string - * example: Valid phone number, message content, and sender ID must be provided. - * 404: - * description: Sender not found. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 404 - * message: - * type: string - * example: Sender not found. - * 500: - * description: Server error. - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: unsuccessful - * status_code: - * type: integer - * example: 500 - * message: - * type: string - * example: Failed to send SMS. Please try again later. - */ -import { Request, Response } from "express"; -import AppDataSource from "../data-source"; -import { User } from "../models"; -import { addSmsToQueue } from "../utils/queue"; -import parsePhoneNumberFromString, { - isPossiblePhoneNumber, - PhoneNumber, -} from "libphonenumber-js"; - -export const sendSms = async (req: Request, res: Response): Promise => { - const { phone_number, message } = req.body; - const sender_id = req.user.id; - - if (!phone_number || !message || !sender_id) { - res.status(400).json({ - status: "unsuccessful", - status_code: 400, - message: - "Valid phone number, message content, and sender ID must be provided.", - }); - return; - } - - let parsedPhone: PhoneNumber; - try { - parsedPhone = parsePhoneNumberFromString(phone_number); - - if (!parsedPhone || !parsedPhone.isValid()) { - const defaultCountryCode = "NG"; - parsedPhone = parsePhoneNumberFromString( - phone_number, - defaultCountryCode, - ); - } - } catch (error) { - res.status(422).json({ - errors: [ - { - field: "phone", - message: "Phone must be a valid international or local number", - }, - ], - }); - return; - } - - if ( - !parsedPhone || - !parsedPhone.isValid() || - !isPossiblePhoneNumber(parsedPhone.number) || - parsedPhone.number.length < 8 || - parsedPhone.number.length > 15 - ) { - res.status(422).json({ - errors: [ - { - field: "phone", - message: "Phone must be a valid international or local number", - }, - ], - }); - return; - } - - try { - const userRepository = AppDataSource.getRepository(User); - const sender = await userRepository.findOneBy({ id: sender_id }); - - if (!sender) { - res.status(404).json({ - status: "unsuccessful", - status_code: 404, - message: "Sender not found.", - }); - return; - } - - await addSmsToQueue({ - sender_id, - message, - phone_number: parsedPhone.number, - }); - res.status(200).json({ - status: "success", - status_code: 200, - message: "SMS added to the queue successfully.", - }); - } catch (error) { - console.error("Error adding SMS to queue:", error); - res.status(500).json({ - status: "unsuccessful", - status_code: 500, - message: "Failed to send SMS. Please try again later.", - }); - } -}; +/** + * @swagger + * /api/v1/sms/send: + * post: + * summary: Send an SMS + * description: Sends an SMS message to the specified phone number. + * tags: [SMS] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - phone_number + * - message + * properties: + * phone_number: + * type: string + * example: "+23465544466" + * description: The phone number to send the SMS to. + * message: + * type: string + * example: "Hello, this is a test message." + * description: The content of the SMS message. + * responses: + * 200: + * description: SMS added to the queue successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * status_code: + * type: integer + * example: 200 + * message: + * type: string + * example: SMS added to the queue successfully. + * 400: + * description: Invalid input data. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 400 + * message: + * type: string + * example: Valid phone number, message content, and sender ID must be provided. + * 404: + * description: Sender not found. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 404 + * message: + * type: string + * example: Sender not found. + * 500: + * description: Server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: integer + * example: 500 + * message: + * type: string + * example: Failed to send SMS. Please try again later. + */ +import { Request, Response } from "express"; +import AppDataSource from "../data-source"; +import { User } from "../models"; +import { addSmsToQueue } from "../utils/queue"; +import parsePhoneNumberFromString, { + isPossiblePhoneNumber, + PhoneNumber, +} from "libphonenumber-js"; + +export const sendSms = async (req: Request, res: Response): Promise => { + const { phone_number, message } = req.body; + const sender_id = req.user.id; + + if (!phone_number || !message || !sender_id) { + res.status(400).json({ + status: "unsuccessful", + status_code: 400, + message: + "Valid phone number, message content, and sender ID must be provided.", + }); + return; + } + + let parsedPhone: PhoneNumber; + try { + parsedPhone = parsePhoneNumberFromString(phone_number); + + if (!parsedPhone || !parsedPhone.isValid()) { + const defaultCountryCode = "NG"; + parsedPhone = parsePhoneNumberFromString( + phone_number, + defaultCountryCode, + ); + } + } catch (error) { + res.status(422).json({ + errors: [ + { + field: "phone", + message: "Phone must be a valid international or local number", + }, + ], + }); + return; + } + + if ( + !parsedPhone || + !parsedPhone.isValid() || + !isPossiblePhoneNumber(parsedPhone.number) || + parsedPhone.number.length < 8 || + parsedPhone.number.length > 15 + ) { + res.status(422).json({ + errors: [ + { + field: "phone", + message: "Phone must be a valid international or local number", + }, + ], + }); + return; + } + + try { + const userRepository = AppDataSource.getRepository(User); + const sender = await userRepository.findOneBy({ id: sender_id }); + + if (!sender) { + res.status(404).json({ + status: "unsuccessful", + status_code: 404, + message: "Sender not found.", + }); + return; + } + + await addSmsToQueue({ + sender_id, + message, + phone_number: parsedPhone.number, + }); + res.status(200).json({ + status: "success", + status_code: 200, + message: "SMS added to the queue successfully.", + }); + } catch (error) { + console.error("Error adding SMS to queue:", error); + res.status(500).json({ + status: "unsuccessful", + status_code: 500, + message: "Failed to send SMS. Please try again later.", + }); + } +}; diff --git a/src/controllers/TestimonialsController.ts b/src/controllers/TestimonialsController.ts index a1982b36..d65f1ca7 100644 --- a/src/controllers/TestimonialsController.ts +++ b/src/controllers/TestimonialsController.ts @@ -1,175 +1,175 @@ -import { Request, Response } from "express"; -import AppDataSource from "../data-source"; -import { Testimonial } from "../models/Testimonial"; - -export default class TestimonialsController { - /** - * @swagger - * tags: - * name: Testimonials - * description: Testimonial related routes - */ - - /** - * @swagger - * api/v1/testimonials: - * post: - * summary: Create a new testimonial - * tags: [Testimonials] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * client_name: - * type: string - * client_position: - * type: string - * testimonial: - * type: string - * responses: - * 201: - * description: Testimonial created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * status_code: - * type: integer - * data: - * type: object - * 500: - * description: Some server error - */ - public async createTestimonial(req: Request, res: Response) { - const { client_name, client_position, testimonial } = req.body; - - // Get the user ID from the request - const userId = (req as Record).user.id; - - // Create a new testimonial - const testimonialInstance = AppDataSource.getRepository(Testimonial).create( - { - user_id: userId, - client_name, - client_position, - testimonial, - } - ); - - // Save the testimonial - await AppDataSource.getRepository(Testimonial).save(testimonialInstance); - - // Return the testimonial - res.status(201).json({ - message: "Testimonial created successfully", - status_code: 201, - data: testimonialInstance, - }); - } - - /** - * @swagger - * api/v1/testimonials/{id}: - * get: - * summary: Retrieve a testimonial by ID - * tags: [Testimonials] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * description: Testimonial ID - * responses: - * 200: - * description: Testimonial retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * status_code: - * type: integer - * data: - * type: object - * 404: - * description: Testimonial not found - * 500: - * description: Some server error - */ - - public async getTestimonial(req: Request, res: Response) { - try { - // Get the user ID from the request - // const userId = (req as Record).user.id; - const { testimonial_id } = req.params; - - // Get the testimonial - const testimonial = await AppDataSource.getRepository( - Testimonial - ).findOne({ where: { id: testimonial_id } }); - - if (!testimonial) { - return res - .status(404) - .send({ message: "Testimonial not found", status_code: 404 }); - } - - // Return the testimonial - res.status(200).json({ - message: "Testimonial retrieved successfully", - status_code: 200, - data: testimonial, - }); - } catch (error) { - res.status(500).send({ message: error.message }); - } - } - - // CODE BY TOMILLA OLUWAFEMI - public async getAllTestimonials(req: Request, res: Response) { - try { - const testimonials = await AppDataSource.getRepository(Testimonial).find(); - res.status(200).json({ - message: "Testimonials retrieved successfully", - status_code: 200, - data: testimonials, - }); - } catch (error) { - res.status(500).send({ message: error.message }); - } - } - - public async deleteTestimonial(req: Request, res: Response) { - try { - const { testimonial_id } = req.params; - - const testimonialToDelete = await AppDataSource.getRepository(Testimonial).findOne({ - where: { id: testimonial_id }, - }); - - if (!testimonialToDelete) { - return res - .status(404) - .send({ message: "Testimonial not found", status_code: 404 }); - } - - await AppDataSource.getRepository(Testimonial).remove(testimonialToDelete); - - res.status(200).json({ - message: "Testimonial deleted successfully", - status_code: 200, - }); - } catch (error) { - res.status(500).send({ message: error.message }); - } - } -} +import { Request, Response } from "express"; +import AppDataSource from "../data-source"; +import { Testimonial } from "../models/Testimonial"; + +export default class TestimonialsController { + /** + * @swagger + * tags: + * name: Testimonials + * description: Testimonial related routes + */ + + /** + * @swagger + * api/v1/testimonials: + * post: + * summary: Create a new testimonial + * tags: [Testimonials] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * client_name: + * type: string + * client_position: + * type: string + * testimonial: + * type: string + * responses: + * 201: + * description: Testimonial created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * status_code: + * type: integer + * data: + * type: object + * 500: + * description: Some server error + */ + public async createTestimonial(req: Request, res: Response) { + const { client_name, client_position, testimonial } = req.body; + + // Get the user ID from the request + const userId = (req as Record).user.id; + + // Create a new testimonial + const testimonialInstance = AppDataSource.getRepository(Testimonial).create( + { + user_id: userId, + client_name, + client_position, + testimonial, + } + ); + + // Save the testimonial + await AppDataSource.getRepository(Testimonial).save(testimonialInstance); + + // Return the testimonial + res.status(201).json({ + message: "Testimonial created successfully", + status_code: 201, + data: testimonialInstance, + }); + } + + /** + * @swagger + * api/v1/testimonials/{id}: + * get: + * summary: Retrieve a testimonial by ID + * tags: [Testimonials] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Testimonial ID + * responses: + * 200: + * description: Testimonial retrieved successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * status_code: + * type: integer + * data: + * type: object + * 404: + * description: Testimonial not found + * 500: + * description: Some server error + */ + + public async getTestimonial(req: Request, res: Response) { + try { + // Get the user ID from the request + // const userId = (req as Record).user.id; + const { testimonial_id } = req.params; + + // Get the testimonial + const testimonial = await AppDataSource.getRepository( + Testimonial + ).findOne({ where: { id: testimonial_id } }); + + if (!testimonial) { + return res + .status(404) + .send({ message: "Testimonial not found", status_code: 404 }); + } + + // Return the testimonial + res.status(200).json({ + message: "Testimonial retrieved successfully", + status_code: 200, + data: testimonial, + }); + } catch (error) { + res.status(500).send({ message: error.message }); + } + } + + // CODE BY TOMILLA OLUWAFEMI + public async getAllTestimonials(req: Request, res: Response) { + try { + const testimonials = await AppDataSource.getRepository(Testimonial).find(); + res.status(200).json({ + message: "Testimonials retrieved successfully", + status_code: 200, + data: testimonials, + }); + } catch (error) { + res.status(500).send({ message: error.message }); + } + } + + public async deleteTestimonial(req: Request, res: Response) { + try { + const { testimonial_id } = req.params; + + const testimonialToDelete = await AppDataSource.getRepository(Testimonial).findOne({ + where: { id: testimonial_id }, + }); + + if (!testimonialToDelete) { + return res + .status(404) + .send({ message: "Testimonial not found", status_code: 404 }); + } + + await AppDataSource.getRepository(Testimonial).remove(testimonialToDelete); + + res.status(200).json({ + message: "Testimonial deleted successfully", + status_code: 200, + }); + } catch (error) { + res.status(500).send({ message: error.message }); + } + } +} diff --git a/src/controllers/billingController.ts b/src/controllers/billingController.ts index 108a087f..d0315d92 100644 --- a/src/controllers/billingController.ts +++ b/src/controllers/billingController.ts @@ -1,15 +1,15 @@ -import { Request, Response } from "express"; -import { BillingService } from "../services/billing-plans.services"; - -const billingService = new BillingService(); - -export class BillingController { - async getAllBillings(req: Request, res: Response) { - try { - const billing = await billingService.getAllBillingPlans(); - res.status(201).json({ message: "Success", billing }); - } catch (error) { - res.status(500).json({ message: error.message }); - } - } -} +import { Request, Response } from "express"; +import { BillingService } from "../services/billing-plans.services"; + +const billingService = new BillingService(); + +export class BillingController { + async getAllBillings(req: Request, res: Response) { + try { + const billing = await billingService.getAllBillingPlans(); + res.status(201).json({ message: "Success", billing }); + } catch (error) { + res.status(500).json({ message: error.message }); + } + } +} diff --git a/src/controllers/billingplanController.ts b/src/controllers/billingplanController.ts index 61d3ec1f..41bf5cb7 100644 --- a/src/controllers/billingplanController.ts +++ b/src/controllers/billingplanController.ts @@ -1,162 +1,162 @@ -import { NextFunction, Request, Response } from "express"; -import { BillingPlanService } from "../services/billingplan.services"; -import { HttpError } from "../middleware"; - -export class BillingPlanController { - private billingPlanService: BillingPlanService; - - constructor() { - this.billingPlanService = new BillingPlanService(); - this.createBillingPlan = this.createBillingPlan.bind(this); - this.getBillingPlans = this.getBillingPlans.bind(this); - } - - /** - * @swagger - * /api/v1/billing-plans: - * post: - * summary: Create a new billing plan - * description: Creates a new billing plan with the provided details - * tags: [Billing Plan] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * - organizationId - * - price - * properties: - * name: - * type: string - * example: "hello" - * organizationId: - * type: string - * example: "a73449ef-7d16-4a72-981a-79016f30735c" - * price: - * type: number - * example: 5 - * responses: - * 201: - * description: Successful response - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: string - * example: "successful" - * status_code: - * type: number - * example: 201 - * data: - * type: object - * properties: - * id: - * type: string - * example: "7880f784-c86c-4abf-b19c-c25720fbfb7f" - * name: - * type: string - * example: "hello" - * organizationId: - * type: string - * example: "a73449ef-7d16-4a72-981a-79016f30735c" - * price: - * type: number - * example: 5 - */ - - async createBillingPlan(req: Request, res: Response, next: NextFunction) { - try { - const planData = req.body; - const createdPlan = - await this.billingPlanService.createBillingPlan(planData); - res.status(201).json({ - success: "successful", - status_code: 201, - data: createdPlan, - }); - } catch (error) { - next(new HttpError(500, error.message)); - } - } - - /** - * @swagger - * /api/v1/billing-plans/{id}: - * get: - * summary: Get a billing plan by ID - * description: Retrieves a specific billing plan by its ID - * tags: [Billing Plan] - * parameters: - * - in: path - * name: id - * required: true - * description: ID of the billing plan to retrieve - * schema: - * type: string - * responses: - * 200: - * description: Successful response - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: string - * example: "successful" - * status_code: - * type: number - * example: 200 - * data: - * type: object - * properties: - * id: - * type: string - * example: "6b792203-dc65-475c-8733-2d018b9e3c7c" - * organizationId: - * type: string - * example: "a73449ef-7d16-4a72-981a-79016f30735c" - * name: - * type: string - * example: "hello" - * price: - * type: string - * example: "4.00" - * currency: - * type: string - * example: "USD" - * duration: - * type: string - * example: "monthly" - * description: - * type: string - * nullable: true - * example: null - * features: - * type: array - * items: - * type: string - * example: [] - * 500: - * description: Internal Server Error - */ - - async getBillingPlans(req: Request, res: Response, next: NextFunction) { - try { - const planId = req.params.id; - const plan = await this.billingPlanService.getBillingPlan(planId); - res.status(200).json({ - success: "successful", - status_code: 200, - data: plan, - }); - } catch (error) { - next(new HttpError(500, error.message)); - } - } -} +import { NextFunction, Request, Response } from "express"; +import { BillingPlanService } from "../services/billingplan.services"; +import { HttpError } from "../middleware"; + +export class BillingPlanController { + private billingPlanService: BillingPlanService; + + constructor() { + this.billingPlanService = new BillingPlanService(); + this.createBillingPlan = this.createBillingPlan.bind(this); + this.getBillingPlans = this.getBillingPlans.bind(this); + } + + /** + * @swagger + * /api/v1/billing-plans: + * post: + * summary: Create a new billing plan + * description: Creates a new billing plan with the provided details + * tags: [Billing Plan] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - organizationId + * - price + * properties: + * name: + * type: string + * example: "hello" + * organizationId: + * type: string + * example: "a73449ef-7d16-4a72-981a-79016f30735c" + * price: + * type: number + * example: 5 + * responses: + * 201: + * description: Successful response + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: string + * example: "successful" + * status_code: + * type: number + * example: 201 + * data: + * type: object + * properties: + * id: + * type: string + * example: "7880f784-c86c-4abf-b19c-c25720fbfb7f" + * name: + * type: string + * example: "hello" + * organizationId: + * type: string + * example: "a73449ef-7d16-4a72-981a-79016f30735c" + * price: + * type: number + * example: 5 + */ + + async createBillingPlan(req: Request, res: Response, next: NextFunction) { + try { + const planData = req.body; + const createdPlan = + await this.billingPlanService.createBillingPlan(planData); + res.status(201).json({ + success: "successful", + status_code: 201, + data: createdPlan, + }); + } catch (error) { + next(new HttpError(500, error.message)); + } + } + + /** + * @swagger + * /api/v1/billing-plans/{id}: + * get: + * summary: Get a billing plan by ID + * description: Retrieves a specific billing plan by its ID + * tags: [Billing Plan] + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the billing plan to retrieve + * schema: + * type: string + * responses: + * 200: + * description: Successful response + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: string + * example: "successful" + * status_code: + * type: number + * example: 200 + * data: + * type: object + * properties: + * id: + * type: string + * example: "6b792203-dc65-475c-8733-2d018b9e3c7c" + * organizationId: + * type: string + * example: "a73449ef-7d16-4a72-981a-79016f30735c" + * name: + * type: string + * example: "hello" + * price: + * type: string + * example: "4.00" + * currency: + * type: string + * example: "USD" + * duration: + * type: string + * example: "monthly" + * description: + * type: string + * nullable: true + * example: null + * features: + * type: array + * items: + * type: string + * example: [] + * 500: + * description: Internal Server Error + */ + + async getBillingPlans(req: Request, res: Response, next: NextFunction) { + try { + const planId = req.params.id; + const plan = await this.billingPlanService.getBillingPlan(planId); + res.status(200).json({ + success: "successful", + status_code: 200, + data: plan, + }); + } catch (error) { + next(new HttpError(500, error.message)); + } + } +} diff --git a/src/controllers/contactController.ts b/src/controllers/contactController.ts index 098eeb61..84949319 100644 --- a/src/controllers/contactController.ts +++ b/src/controllers/contactController.ts @@ -1,172 +1,172 @@ -import { Request, Response } from "express"; -import { ContactService } from "../services/contactService"; -import { validateContact } from "../utils/contactValidator"; - -const contactService = new ContactService(); - -/** - * @swagger - * /api/v1/contact: - * post: - * summary: Submit a contact form - * description: Allows users to submit their contact details and message. - * tags: - * - Contact - * requestBody: - * description: Contact details and message - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * example: John Doe - * email: - * type: string - * format: email - * example: johndoe@example.com - * message: - * type: string - * example: I would like to inquire about your services. - * required: - * - name - * - email - * - message - * responses: - * 200: - * description: Contact submitted successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Contact submitted successfully - * contact: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * name: - * type: string - * example: John Doe - * email: - * type: string - * example: johndoe@example.com - * message: - * type: string - * example: I would like to inquire about your services. - * 400: - * description: Bad request, validation failed - * content: - * application/json: - * schema: - * type: object - * properties: - * errors: - * type: array - * items: - * type: string - * example: [ "Please enter a valid email address." ] - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: Internal server error - */ - -export class ContactController { - async createContact(req: Request, res: Response): Promise { - const { name, email, message } = req.body; - - const validationErrors = validateContact({ - name, - email, - message, - }); - if (validationErrors.length > 0) { - res.status(400).json({ errors: validationErrors }); - return; - } - - try { - const contact = await contactService.createContact({ - name, - email, - message, - }); - res - .status(200) - .json({ message: "Message submitted successfully", contact }); - } catch (error) { - res.status(500).json({ error: "Internal server error" }); - } - } - - /** - * @swagger - * /api/v1/contact: - * get: - * summary: Retrieve all contact messages - * description: Fetches all contact messages submitted via the contact form. - * tags: - * - Contact - * responses: - * 200: - * description: A list of contact messages - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Success - * contacts: - * type: array - * items: - * type: object - * properties: - * id: - * type: integer - * example: 1 - * name: - * type: string - * example: John Doe - * email: - * type: string - * format: email - * example: johndoe@example.com - * message: - * type: string - * example: I would like to inquire about your services. - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: Internal server error - */ - - async getAllContact(req: Request, res: Response) { - try { - const contact = await contactService.getAllContactUs(); - res.status(200).json({ message: "Success", contact }); - } catch (error) { - res.status(500).json({ message: error.message }); - } - } -} +import { Request, Response } from "express"; +import { ContactService } from "../services/contactService"; +import { validateContact } from "../utils/contactValidator"; + +const contactService = new ContactService(); + +/** + * @swagger + * /api/v1/contact: + * post: + * summary: Submit a contact form + * description: Allows users to submit their contact details and message. + * tags: + * - Contact + * requestBody: + * description: Contact details and message + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * example: John Doe + * email: + * type: string + * format: email + * example: johndoe@example.com + * message: + * type: string + * example: I would like to inquire about your services. + * required: + * - name + * - email + * - message + * responses: + * 200: + * description: Contact submitted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Contact submitted successfully + * contact: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * name: + * type: string + * example: John Doe + * email: + * type: string + * example: johndoe@example.com + * message: + * type: string + * example: I would like to inquire about your services. + * 400: + * description: Bad request, validation failed + * content: + * application/json: + * schema: + * type: object + * properties: + * errors: + * type: array + * items: + * type: string + * example: [ "Please enter a valid email address." ] + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: Internal server error + */ + +export class ContactController { + async createContact(req: Request, res: Response): Promise { + const { name, email, message } = req.body; + + const validationErrors = validateContact({ + name, + email, + message, + }); + if (validationErrors.length > 0) { + res.status(400).json({ errors: validationErrors }); + return; + } + + try { + const contact = await contactService.createContact({ + name, + email, + message, + }); + res + .status(200) + .json({ message: "Message submitted successfully", contact }); + } catch (error) { + res.status(500).json({ error: "Internal server error" }); + } + } + + /** + * @swagger + * /api/v1/contact: + * get: + * summary: Retrieve all contact messages + * description: Fetches all contact messages submitted via the contact form. + * tags: + * - Contact + * responses: + * 200: + * description: A list of contact messages + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Success + * contacts: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * name: + * type: string + * example: John Doe + * email: + * type: string + * format: email + * example: johndoe@example.com + * message: + * type: string + * example: I would like to inquire about your services. + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Internal server error + */ + + async getAllContact(req: Request, res: Response) { + try { + const contact = await contactService.getAllContactUs(); + res.status(200).json({ message: "Success", contact }); + } catch (error) { + res.status(500).json({ message: error.message }); + } + } +} diff --git a/src/controllers/exportController.ts b/src/controllers/exportController.ts index d3050530..61a90786 100644 --- a/src/controllers/exportController.ts +++ b/src/controllers/exportController.ts @@ -1,74 +1,74 @@ -// src/controllers/exportController.ts -import { Request, Response } from "express"; -import ExportService from "../services/export.services"; -/** - * @swagger - * /api/v1/organisation/members/export: - * get: - * summary: Export signed-in user information - * tags: [Export user data by csv or pdf format] - * parameters: - * - in: query - * name: format - * schema: - * type: string - * enum: [csv, pdf] - * required: true - * description: The format to export the user data in. - * responses: - * 200: - * description: User data exported successfully. - * content: - * text/csv: - * schema: - * type: string - * format: binary - * application/pdf: - * schema: - * type: string - * format: binary - * 400: - * description: Invalid format. - * 401: - * description: Unauthorized. No token provided or token is invalid. - * 404: - * description: User not found. - * 500: - * description: Internal server error. - * security: - * - bearerAuth: [] - */ - -class exportController { - static exportData = async (req: Request, res: Response) => { - try { - const format = req.query.format as string; - const userId = req.user.id; - - const user = await ExportService.getUserById(userId); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - - const users = [user]; - - if (format === "csv") { - const csv = ExportService.generateCSV(users); - res.header("Content-Type", "text/csv"); - res.attachment("users.csv"); - return res.send(csv); - } else if (format === "pdf") { - const pdf = await ExportService.generatePDF(users); // await here - res.header("Content-Type", "application/pdf"); - res.attachment("users.pdf"); - return res.send(pdf); - } else { - return res.status(400).send("Invalid format"); - } - } catch (error) { - res.status(500).json({ message: error.message }); - } - }; -} - -export default exportController; +// src/controllers/exportController.ts +import { Request, Response } from "express"; +import ExportService from "../services/export.services"; +/** + * @swagger + * /api/v1/organisation/members/export: + * get: + * summary: Export signed-in user information + * tags: [Export user data by csv or pdf format] + * parameters: + * - in: query + * name: format + * schema: + * type: string + * enum: [csv, pdf] + * required: true + * description: The format to export the user data in. + * responses: + * 200: + * description: User data exported successfully. + * content: + * text/csv: + * schema: + * type: string + * format: binary + * application/pdf: + * schema: + * type: string + * format: binary + * 400: + * description: Invalid format. + * 401: + * description: Unauthorized. No token provided or token is invalid. + * 404: + * description: User not found. + * 500: + * description: Internal server error. + * security: + * - bearerAuth: [] + */ + +class exportController { + static exportData = async (req: Request, res: Response) => { + try { + const format = req.query.format as string; + const userId = req.user.id; + + const user = await ExportService.getUserById(userId); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + const users = [user]; + + if (format === "csv") { + const csv = ExportService.generateCSV(users); + res.header("Content-Type", "text/csv"); + res.attachment("users.csv"); + return res.send(csv); + } else if (format === "pdf") { + const pdf = await ExportService.generatePDF(users); // await here + res.header("Content-Type", "application/pdf"); + res.attachment("users.pdf"); + return res.send(pdf); + } else { + return res.status(400).send("Invalid format"); + } + } catch (error) { + res.status(500).json({ message: error.message }); + } + }; +} + +export default exportController; diff --git a/src/controllers/paymentStripeController.ts b/src/controllers/paymentStripeController.ts index 49d72e09..a2c84e8e 100644 --- a/src/controllers/paymentStripeController.ts +++ b/src/controllers/paymentStripeController.ts @@ -1,54 +1,54 @@ -/** - * Initiates a payment using Stripe - * @param req - Express request object - * @param res - Express response object - */ -import { Request, Response } from "express"; -import Stripe from "stripe"; -import dotenv from "dotenv"; -import log from "../utils/logger"; - -dotenv.config(); -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: "2024-06-20", -}); -// log.info(`Stripe secret key: ${process.env.STRIPE_SECRET_KEY}`); - -type PaymentRequest = { - payer_type: string; - payer_id: string; - amount: number; - currency: string; -}; -export const createPaymentIntentStripe = async ( - req: Request, - res: Response, -) => { - try { - const { payer_type, payer_id, amount, currency } = req.body; - - const paymentIntent = await stripe.paymentIntents.create({ - amount: Math.round(amount * 100), - currency, - payment_method_types: ["card"], - }); - - res.json({ - status: "success", - message: "Payment successful", - data: { - user: { - payer_type: payer_type, - payment_id: paymentIntent.id, - status: paymentIntent.status, - }, - }, - }); - } catch (error) { - res.status(500).json({ - error: "Internal Server Error", - message: "An unexpected error occurred. Please try again later.", - status_code: 500, - }); - } -}; +/** + * Initiates a payment using Stripe + * @param req - Express request object + * @param res - Express response object + */ +import { Request, Response } from "express"; +import Stripe from "stripe"; +import dotenv from "dotenv"; +import log from "../utils/logger"; + +dotenv.config(); +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: "2024-06-20", +}); +// log.info(`Stripe secret key: ${process.env.STRIPE_SECRET_KEY}`); + +type PaymentRequest = { + payer_type: string; + payer_id: string; + amount: number; + currency: string; +}; +export const createPaymentIntentStripe = async ( + req: Request, + res: Response, +) => { + try { + const { payer_type, payer_id, amount, currency } = req.body; + + const paymentIntent = await stripe.paymentIntents.create({ + amount: Math.round(amount * 100), + currency, + payment_method_types: ["card"], + }); + + res.json({ + status: "success", + message: "Payment successful", + data: { + user: { + payer_type: payer_type, + payment_id: paymentIntent.id, + status: paymentIntent.status, + }, + }, + }); + } catch (error) { + res.status(500).json({ + error: "Internal Server Error", + message: "An unexpected error occurred. Please try again later.", + status_code: 500, + }); + } +}; diff --git a/src/controllers/roleController.ts b/src/controllers/roleController.ts index 844abd9c..328b57f3 100644 --- a/src/controllers/roleController.ts +++ b/src/controllers/roleController.ts @@ -1,175 +1,175 @@ -import { Request, Response, NextFunction } from "express"; -import { User } from "../models"; -import { UserRole } from "../enums/userRoles"; -import { ResourceNotFound, HttpError } from "../middleware/error"; -import { createRole } from "../services/role.services"; - -/** - * @swagger - * /api/roles/{user_id}/{organization_id}: - * put: - * summary: Change the role of a user within an organization - * tags: [Roles] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: user_id - * required: true - * schema: - * type: string - * description: The ID of the user whose role is to be changed - * - in: path - * name: organization_id - * required: true - * schema: - * type: string - * description: The ID of the organization in which the user's role is to be changed - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * new_role: - * type: string - * enum: [admin, user, super_admin] - * description: The new role to assign to the user - * responses: - * 200: - * description: Role updated successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * organization_id: - * type: string - * user_id: - * type: string - * new_role: - * type: string - * 400: - * description: Bad request (Invalid role or user not in organization) - * 404: - * description: User not found - * 500: - * description: Internal server error - */ - -export const changeUserRole = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const { user_id, organization_id } = req.params; - const { new_role } = req.body; - - // Validate the provided role - if (!Object.values(UserRole).includes(new_role)) { - throw new HttpError(400, "Invalid role specified"); - } - - // Retrieve the user whose role needs to be updated - const user = await User.findOne({ - where: { id: user_id }, - relations: ["organizations"], - }); - - if (!user) { - throw new ResourceNotFound("User not found"); - } - - // Check if the user belongs to the specified organization - const userOrganization = user.organizations.find( - (org) => org.id === organization_id, - ); - - if (!userOrganization) { - throw new HttpError(400, "User does not belong to the specified team"); - } - - // Update the user's role - user.role = new_role; - await user.save(); - - res.status(200).json({ - message: "Team member role updated successfully", - organization_id, - user_id, - new_role, - }); - } catch (error) { - next(error); - } -}; - -/** - * @swagger - * /api/v1/role: - * post: - * summary: Create a new role for the authenticated user - * tags: [Roles] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - new_role - * properties: - * new_role: - * type: string - * enum: [user, admin, super_admin] - * responses: - * 200: - * description: User role created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * user_id: - * type: string - * new_role: - * type: string - * example: - * message: "User role created successfully" - * user_id: "123e4567-e89b-12d3-a456-426614174000" - * new_role: "admin" - * 400: - * description: Bad request (Invalid role) - * 401: - * description: Unauthorized - * 500: - * description: Internal server error - */ - -export const createUserRole = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const user_id = req.user.id; - const { new_role } = req.body; - - const updatedUser = await createRole(user_id, new_role); - res.status(200).json({ - message: "User role created successfully", - user_id, - new_role: updatedUser.role, - }); - } catch (error) { - next(error); - } -}; +import { Request, Response, NextFunction } from "express"; +import { User } from "../models"; +import { UserRole } from "../enums/userRoles"; +import { ResourceNotFound, HttpError } from "../middleware/error"; +import { createRole } from "../services/role.services"; + +/** + * @swagger + * /api/roles/{user_id}/{organization_id}: + * put: + * summary: Change the role of a user within an organization + * tags: [Roles] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: user_id + * required: true + * schema: + * type: string + * description: The ID of the user whose role is to be changed + * - in: path + * name: organization_id + * required: true + * schema: + * type: string + * description: The ID of the organization in which the user's role is to be changed + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * new_role: + * type: string + * enum: [admin, user, super_admin] + * description: The new role to assign to the user + * responses: + * 200: + * description: Role updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * organization_id: + * type: string + * user_id: + * type: string + * new_role: + * type: string + * 400: + * description: Bad request (Invalid role or user not in organization) + * 404: + * description: User not found + * 500: + * description: Internal server error + */ + +export const changeUserRole = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { user_id, organization_id } = req.params; + const { new_role } = req.body; + + // Validate the provided role + if (!Object.values(UserRole).includes(new_role)) { + throw new HttpError(400, "Invalid role specified"); + } + + // Retrieve the user whose role needs to be updated + const user = await User.findOne({ + where: { id: user_id }, + relations: ["organizations"], + }); + + if (!user) { + throw new ResourceNotFound("User not found"); + } + + // Check if the user belongs to the specified organization + const userOrganization = user.organizations.find( + (org) => org.id === organization_id, + ); + + if (!userOrganization) { + throw new HttpError(400, "User does not belong to the specified team"); + } + + // Update the user's role + user.role = new_role; + await user.save(); + + res.status(200).json({ + message: "Team member role updated successfully", + organization_id, + user_id, + new_role, + }); + } catch (error) { + next(error); + } +}; + +/** + * @swagger + * /api/v1/role: + * post: + * summary: Create a new role for the authenticated user + * tags: [Roles] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - new_role + * properties: + * new_role: + * type: string + * enum: [user, admin, super_admin] + * responses: + * 200: + * description: User role created successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * user_id: + * type: string + * new_role: + * type: string + * example: + * message: "User role created successfully" + * user_id: "123e4567-e89b-12d3-a456-426614174000" + * new_role: "admin" + * 400: + * description: Bad request (Invalid role) + * 401: + * description: Unauthorized + * 500: + * description: Internal server error + */ + +export const createUserRole = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const user_id = req.user.id; + const { new_role } = req.body; + + const updatedUser = await createRole(user_id, new_role); + res.status(200).json({ + message: "User role created successfully", + user_id, + new_role: updatedUser.role, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/runTestController.ts b/src/controllers/runTestController.ts index a5005718..6ef37511 100644 --- a/src/controllers/runTestController.ts +++ b/src/controllers/runTestController.ts @@ -1,35 +1,35 @@ -import { exec } from "child_process"; -import { Request, Response } from "express"; - -export const runTestController = async (req: Request, res: Response) => { - exec( - "python3 src/controllers/tests/tests/run_tests.py", - (error, stdout, stderr) => { - if (stderr) { - res.status(500).json({ - status_code: 500, - message: "Script error", - error: stderr, - }); - } else if (error) { - res.status(500).json({ - status_code: 500, - message: "Script error", - error: error, - }); - } else { - const formattedOutput = stdout - .split("\n") - .map((line) => line.trim()) - .filter((line) => line.length > 0) - .join("\n"); - - res.status(200).json({ - status_code: 200, - message: "Script executed successfully", - data: formattedOutput, - }); - } - }, - ); -}; +import { exec } from "child_process"; +import { Request, Response } from "express"; + +export const runTestController = async (req: Request, res: Response) => { + exec( + "python3 src/controllers/tests/tests/run_tests.py", + (error, stdout, stderr) => { + if (stderr) { + res.status(500).json({ + status_code: 500, + message: "Script error", + error: stderr, + }); + } else if (error) { + res.status(500).json({ + status_code: 500, + message: "Script error", + error: error, + }); + } else { + const formattedOutput = stdout + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .join("\n"); + + res.status(200).json({ + status_code: 200, + message: "Script executed successfully", + data: formattedOutput, + }); + } + }, + ); +}; diff --git a/src/controllers/sendEmail.controller.ts b/src/controllers/sendEmail.controller.ts index 1df6fe56..9ca44fe5 100644 --- a/src/controllers/sendEmail.controller.ts +++ b/src/controllers/sendEmail.controller.ts @@ -1,186 +1,186 @@ -/** - * @swagger - * /api/v1/email-templates: - * get: - * tags: - * - Email - * summary: Get all email templates - * description: Retrieve a list of all email templates - * responses: - * 200: - * description: The list of email templates - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/EmailTemplates' - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Internal server error." - */ - -/** - * @swagger - * /api/v1/send-email: - * post: - * tags: - * - Email - * summary: Send an email using a predefined template - * description: Submits an email sending request referencing a specific template. - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * template_id: - * type: string - * example: "account-activation-request" - * recipient: - * type: string - * example: "john.doe@example.com" - * variables: - * type: object - * properties: - * title: - * type: string - * example: "Activate Your Account" - * activationLinkUrl: - * type: string - * example: "https://example.com" - * user_name: - * type: string - * example: "John Doe" - * responses: - * 202: - * description: Email sending request accepted and is being processed in the background. - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Email sending request accepted and is being processed in the background." - * 400: - * description: An invalid request was sent. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "An invalid request was sent." - * 404: - * description: Template not found. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Template not found." - * 405: - * description: This method is not allowed. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "This method is not allowed." - * 500: - * description: Internal server error. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * example: "Internal server error." - */ - -import { Request, Response } from "express"; -import AppDataSource from "../data-source"; -import { User } from "../models"; -import { EmailService } from "../services"; -import { EmailQueuePayload } from "../types"; - -const emailService = new EmailService(); - -export const SendEmail = async (req: Request, res: Response) => { - const { template_id, recipient, variables } = req.body; - if (!template_id || !recipient) { - return res.status(400).json({ - success: false, - status_code: 400, - message: "Invalid input. Template ID and recipient are required.", - }); - } - - const payload: EmailQueuePayload = { - templateId: template_id, - recipient, - variables, - }; - - try { - const availableTemplates: {}[] = await emailService.getEmailTemplates(); - const templateIds = availableTemplates.map( - (template: { templateId: string }) => template.templateId, - ); - - if (!templateIds.includes(template_id)) { - return res.status(400).json({ - success: false, - status_code: 400, - message: "Template not found", - available_templates: templateIds, - }); - } - - const user = await AppDataSource.getRepository(User).findOne({ - where: { email: payload.recipient }, - }); - // if (!user) { - // return res.status(404).json({ - // success: false, - // status_code: 404, - // message: "User not found", - // }); - // } - - await emailService.queueEmail(payload, user); - // await emailService.sendEmail(payload); - - return res.status(202).json({ - message: "Email sending request accepted and is being processed.", - }); - } catch (error) { - return res - .status(500) - .json({ message: "Internal server error.", error: error }); - } -}; - -export const getEmailTemplates = async (req: Request, res: Response) => { - try { - const templates = await emailService.getEmailTemplates(); - return res.status(200).json({ message: "Available templates", templates }); - } catch (error) { - return res.status(500).json({ message: "Internal server error." }); - } -}; +/** + * @swagger + * /api/v1/email-templates: + * get: + * tags: + * - Email + * summary: Get all email templates + * description: Retrieve a list of all email templates + * responses: + * 200: + * description: The list of email templates + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/EmailTemplates' + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Internal server error." + */ + +/** + * @swagger + * /api/v1/send-email: + * post: + * tags: + * - Email + * summary: Send an email using a predefined template + * description: Submits an email sending request referencing a specific template. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * template_id: + * type: string + * example: "account-activation-request" + * recipient: + * type: string + * example: "john.doe@example.com" + * variables: + * type: object + * properties: + * title: + * type: string + * example: "Activate Your Account" + * activationLinkUrl: + * type: string + * example: "https://example.com" + * user_name: + * type: string + * example: "John Doe" + * responses: + * 202: + * description: Email sending request accepted and is being processed in the background. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Email sending request accepted and is being processed in the background." + * 400: + * description: An invalid request was sent. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "An invalid request was sent." + * 404: + * description: Template not found. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Template not found." + * 405: + * description: This method is not allowed. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "This method is not allowed." + * 500: + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Internal server error." + */ + +import { Request, Response } from "express"; +import AppDataSource from "../data-source"; +import { User } from "../models"; +import { EmailService } from "../services"; +import { EmailQueuePayload } from "../types"; + +const emailService = new EmailService(); + +export const SendEmail = async (req: Request, res: Response) => { + const { template_id, recipient, variables } = req.body; + if (!template_id || !recipient) { + return res.status(400).json({ + success: false, + status_code: 400, + message: "Invalid input. Template ID and recipient are required.", + }); + } + + const payload: EmailQueuePayload = { + templateId: template_id, + recipient, + variables, + }; + + try { + const availableTemplates: {}[] = await emailService.getEmailTemplates(); + const templateIds = availableTemplates.map( + (template: { templateId: string }) => template.templateId, + ); + + if (!templateIds.includes(template_id)) { + return res.status(400).json({ + success: false, + status_code: 400, + message: "Template not found", + available_templates: templateIds, + }); + } + + const user = await AppDataSource.getRepository(User).findOne({ + where: { email: payload.recipient }, + }); + // if (!user) { + // return res.status(404).json({ + // success: false, + // status_code: 404, + // message: "User not found", + // }); + // } + + await emailService.queueEmail(payload, user); + // await emailService.sendEmail(payload); + + return res.status(202).json({ + message: "Email sending request accepted and is being processed.", + }); + } catch (error) { + return res + .status(500) + .json({ message: "Internal server error.", error: error }); + } +}; + +export const getEmailTemplates = async (req: Request, res: Response) => { + try { + const templates = await emailService.getEmailTemplates(); + return res.status(200).json({ message: "Available templates", templates }); + } catch (error) { + return res.status(500).json({ message: "Internal server error." }); + } +}; diff --git a/src/controllers/tests/README.md b/src/controllers/tests/README.md index 975adf99..1724921f 100644 --- a/src/controllers/tests/README.md +++ b/src/controllers/tests/README.md @@ -1,104 +1,104 @@ -# API Test Suite README - -## Overview - -This repository contains a set of automated tests for verifying various API endpoints. The tests are implemented using Python and the `pytest` framework, and they cover user registration, authentication, help center topics, testimonials, and more. - -## Prerequisites - -1. **Python**: Ensure Python 3.6 or higher is installed on your system. You can check your Python version by running: - ```sh - python --version - ``` -2. **pip**: Python's package installer should be available. If not, you can install it from the [official pip website](https://pip.pypa.io/en/stable/installation/). - -3. **Requests Library**: Used for making HTTP requests. Install it using: - - ```sh - pip install requests - ``` - -4. **pytest**: Testing framework used to run the tests. Install it using: - - ```sh - pip install pytest - ``` - -5. **Faker**: Library for generating fake data. Install it using: - ```sh - pip install faker - ``` - -## Setup - -1. **Clone the Repository**: - - ```sh - git clone - cd - ``` - -2. **Install Dependencies**: - Ensure all required Python packages are installed. You can use a `requirements.txt` file if available. If not, manually install the dependencies using: - - ```sh - pip install requests pytest faker - ``` - -3. **Configure Environment**: - Modify the `BASE_URL` in the script to point to the correct API endpoint if necessary. - -## Running the Tests - -1. **Navigate to the Test Script Directory**: - Make sure you are in the directory containing the test script (e.g., `test_api.py`). - -2. **Run the Tests**: - Execute the tests using `pytest`: - ```sh - pytest test_api.py - ``` - This command will run all the test functions defined in the script. - -## Test Results - -- **Passing Tests**: You will see output indicating which tests passed successfully. -- **Failing Tests**: Any failures will be reported with details on what went wrong. The `print` statements in the script will also output the responses from the API for debugging purposes. - -## Test Script Overview - -- **Imports**: - - - `requests`: For making HTTP requests to the API. - - `pytest`: For running the test cases. - - `Faker`: For generating random data. - -- **Utility Functions**: - - - `generate_random_user()`: Creates random user data. - - `login_and_get_token()`: Logs in a user and retrieves an authentication token. - -- **Test Cases**: - - **Register User**: Tests user registration. - - **Verify OTP**: Tests OTP verification. - - **Forgot Password**: Tests forgot password functionality. - - **Change Password**: Tests password change functionality. - - **Help Center Topics**: CRUD operations for help center topics. - - **Testimonials**: CRUD operations for testimonials. - - **Topic Comments**: CRUD operations for comments on topics. - -## Troubleshooting - -- **Issues with Dependencies**: - If you encounter issues with installing dependencies, ensure you have the correct versions and check for any compatibility issues. - -- **API Errors**: - Ensure that the API endpoints in the script are correct and that the API server is running and accessible. - -## Contributing - -If you have suggestions or improvements, please fork the repository and submit a pull request. For bug reports or feature requests, open an issue on the repository's GitHub page. - -## License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +# API Test Suite README + +## Overview + +This repository contains a set of automated tests for verifying various API endpoints. The tests are implemented using Python and the `pytest` framework, and they cover user registration, authentication, help center topics, testimonials, and more. + +## Prerequisites + +1. **Python**: Ensure Python 3.6 or higher is installed on your system. You can check your Python version by running: + ```sh + python --version + ``` +2. **pip**: Python's package installer should be available. If not, you can install it from the [official pip website](https://pip.pypa.io/en/stable/installation/). + +3. **Requests Library**: Used for making HTTP requests. Install it using: + + ```sh + pip install requests + ``` + +4. **pytest**: Testing framework used to run the tests. Install it using: + + ```sh + pip install pytest + ``` + +5. **Faker**: Library for generating fake data. Install it using: + ```sh + pip install faker + ``` + +## Setup + +1. **Clone the Repository**: + + ```sh + git clone + cd + ``` + +2. **Install Dependencies**: + Ensure all required Python packages are installed. You can use a `requirements.txt` file if available. If not, manually install the dependencies using: + + ```sh + pip install requests pytest faker + ``` + +3. **Configure Environment**: + Modify the `BASE_URL` in the script to point to the correct API endpoint if necessary. + +## Running the Tests + +1. **Navigate to the Test Script Directory**: + Make sure you are in the directory containing the test script (e.g., `test_api.py`). + +2. **Run the Tests**: + Execute the tests using `pytest`: + ```sh + pytest test_api.py + ``` + This command will run all the test functions defined in the script. + +## Test Results + +- **Passing Tests**: You will see output indicating which tests passed successfully. +- **Failing Tests**: Any failures will be reported with details on what went wrong. The `print` statements in the script will also output the responses from the API for debugging purposes. + +## Test Script Overview + +- **Imports**: + + - `requests`: For making HTTP requests to the API. + - `pytest`: For running the test cases. + - `Faker`: For generating random data. + +- **Utility Functions**: + + - `generate_random_user()`: Creates random user data. + - `login_and_get_token()`: Logs in a user and retrieves an authentication token. + +- **Test Cases**: + - **Register User**: Tests user registration. + - **Verify OTP**: Tests OTP verification. + - **Forgot Password**: Tests forgot password functionality. + - **Change Password**: Tests password change functionality. + - **Help Center Topics**: CRUD operations for help center topics. + - **Testimonials**: CRUD operations for testimonials. + - **Topic Comments**: CRUD operations for comments on topics. + +## Troubleshooting + +- **Issues with Dependencies**: + If you encounter issues with installing dependencies, ensure you have the correct versions and check for any compatibility issues. + +- **API Errors**: + Ensure that the API endpoints in the script are correct and that the API server is running and accessible. + +## Contributing + +If you have suggestions or improvements, please fork the repository and submit a pull request. For bug reports or feature requests, open an issue on the repository's GitHub page. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/src/controllers/tests/assets/style.css b/src/controllers/tests/assets/style.css index 32323b9b..1fc3d4e8 100644 --- a/src/controllers/tests/assets/style.css +++ b/src/controllers/tests/assets/style.css @@ -1,320 +1,320 @@ -body { - font-family: Helvetica, Arial, sans-serif; - font-size: 12px; - /* do not increase min-width as some may use split screens */ - min-width: 800px; - color: #999; -} - -h1 { - font-size: 24px; - color: black; -} - -h2 { - font-size: 16px; - color: black; -} - -p { - color: black; -} - -a { - color: #999; -} - -table { - border-collapse: collapse; -} - -/****************************** - * SUMMARY INFORMATION - ******************************/ -#environment td { - padding: 5px; - border: 1px solid #e6e6e6; - vertical-align: top; -} -#environment tr:nth-child(odd) { - background-color: #f6f6f6; -} -#environment ul { - margin: 0; - padding: 0 20px; -} - -/****************************** - * TEST RESULT COLORS - ******************************/ -span.passed, -.passed .col-result { - color: green; -} - -span.skipped, -span.xfailed, -span.rerun, -.skipped .col-result, -.xfailed .col-result, -.rerun .col-result { - color: orange; -} - -span.error, -span.failed, -span.xpassed, -.error .col-result, -.failed .col-result, -.xpassed .col-result { - color: red; -} - -.col-links__extra { - margin-right: 3px; -} - -/****************************** - * RESULTS TABLE - * - * 1. Table Layout - * 2. Extra - * 3. Sorting items - * - ******************************/ -/*------------------ - * 1. Table Layout - *------------------*/ -#results-table { - border: 1px solid #e6e6e6; - color: #999; - font-size: 12px; - width: 100%; -} -#results-table th, -#results-table td { - padding: 5px; - border: 1px solid #e6e6e6; - text-align: left; -} -#results-table th { - font-weight: bold; -} - -/*------------------ - * 2. Extra - *------------------*/ -.logwrapper { - max-height: 230px; - overflow-y: scroll; - background-color: #e6e6e6; -} -.logwrapper.expanded { - max-height: none; -} -.logwrapper.expanded .logexpander:after { - content: "collapse [-]"; -} -.logwrapper .logexpander { - z-index: 1; - position: sticky; - top: 10px; - width: max-content; - border: 1px solid; - border-radius: 3px; - padding: 5px 7px; - margin: 10px 0 10px calc(100% - 80px); - cursor: pointer; - background-color: #e6e6e6; -} -.logwrapper .logexpander:after { - content: "expand [+]"; -} -.logwrapper .logexpander:hover { - color: #000; - border-color: #000; -} -.logwrapper .log { - min-height: 40px; - position: relative; - top: -50px; - height: calc(100% + 50px); - border: 1px solid #e6e6e6; - color: black; - display: block; - font-family: "Courier New", Courier, monospace; - padding: 5px; - padding-right: 80px; - white-space: pre-wrap; -} - -div.media { - border: 1px solid #e6e6e6; - float: right; - height: 240px; - margin: 0 5px; - overflow: hidden; - width: 320px; -} - -.media-container { - display: grid; - grid-template-columns: 25px auto 25px; - align-items: center; - flex: 1 1; - overflow: hidden; - height: 200px; -} - -.media-container--fullscreen { - grid-template-columns: 0px auto 0px; -} - -.media-container__nav--right, -.media-container__nav--left { - text-align: center; - cursor: pointer; -} - -.media-container__viewport { - cursor: pointer; - text-align: center; - height: inherit; -} -.media-container__viewport img, -.media-container__viewport video { - object-fit: cover; - width: 100%; - max-height: 100%; -} - -.media__name, -.media__counter { - display: flex; - flex-direction: row; - justify-content: space-around; - flex: 0 0 25px; - align-items: center; -} - -.collapsible td:not(.col-links) { - cursor: pointer; -} -.collapsible td:not(.col-links):hover::after { - color: #bbb; - font-style: italic; - cursor: pointer; -} - -.col-result { - width: 130px; -} -.col-result:hover::after { - content: " (hide details)"; -} - -.col-result.collapsed:hover::after { - content: " (show details)"; -} - -#environment-header h2:hover::after { - content: " (hide details)"; - color: #bbb; - font-style: italic; - cursor: pointer; - font-size: 12px; -} - -#environment-header.collapsed h2:hover::after { - content: " (show details)"; - color: #bbb; - font-style: italic; - cursor: pointer; - font-size: 12px; -} - -/*------------------ - * 3. Sorting items - *------------------*/ -.sortable { - cursor: pointer; -} -.sortable.desc:after { - content: " "; - position: relative; - left: 5px; - bottom: -12.5px; - border: 10px solid #4caf50; - border-bottom: 0; - border-left-color: transparent; - border-right-color: transparent; -} -.sortable.asc:after { - content: " "; - position: relative; - left: 5px; - bottom: 12.5px; - border: 10px solid #4caf50; - border-top: 0; - border-left-color: transparent; - border-right-color: transparent; -} - -.hidden, -.summary__reload__button.hidden { - display: none; -} - -.summary__data { - flex: 0 0 550px; -} -.summary__reload { - flex: 1 1; - display: flex; - justify-content: center; -} -.summary__reload__button { - flex: 0 0 300px; - display: flex; - color: white; - font-weight: bold; - background-color: #4caf50; - text-align: center; - justify-content: center; - align-items: center; - border-radius: 3px; - cursor: pointer; -} -.summary__reload__button:hover { - background-color: #46a049; -} -.summary__spacer { - flex: 0 0 550px; -} - -.controls { - display: flex; - justify-content: space-between; -} - -.filters, -.collapse { - display: flex; - align-items: center; -} -.filters button, -.collapse button { - color: #999; - border: none; - background: none; - cursor: pointer; - text-decoration: underline; -} -.filters button:hover, -.collapse button:hover { - color: #ccc; -} - -.filter__label { - margin-right: 10px; -} +body { + font-family: Helvetica, Arial, sans-serif; + font-size: 12px; + /* do not increase min-width as some may use split screens */ + min-width: 800px; + color: #999; +} + +h1 { + font-size: 24px; + color: black; +} + +h2 { + font-size: 16px; + color: black; +} + +p { + color: black; +} + +a { + color: #999; +} + +table { + border-collapse: collapse; +} + +/****************************** + * SUMMARY INFORMATION + ******************************/ +#environment td { + padding: 5px; + border: 1px solid #e6e6e6; + vertical-align: top; +} +#environment tr:nth-child(odd) { + background-color: #f6f6f6; +} +#environment ul { + margin: 0; + padding: 0 20px; +} + +/****************************** + * TEST RESULT COLORS + ******************************/ +span.passed, +.passed .col-result { + color: green; +} + +span.skipped, +span.xfailed, +span.rerun, +.skipped .col-result, +.xfailed .col-result, +.rerun .col-result { + color: orange; +} + +span.error, +span.failed, +span.xpassed, +.error .col-result, +.failed .col-result, +.xpassed .col-result { + color: red; +} + +.col-links__extra { + margin-right: 3px; +} + +/****************************** + * RESULTS TABLE + * + * 1. Table Layout + * 2. Extra + * 3. Sorting items + * + ******************************/ +/*------------------ + * 1. Table Layout + *------------------*/ +#results-table { + border: 1px solid #e6e6e6; + color: #999; + font-size: 12px; + width: 100%; +} +#results-table th, +#results-table td { + padding: 5px; + border: 1px solid #e6e6e6; + text-align: left; +} +#results-table th { + font-weight: bold; +} + +/*------------------ + * 2. Extra + *------------------*/ +.logwrapper { + max-height: 230px; + overflow-y: scroll; + background-color: #e6e6e6; +} +.logwrapper.expanded { + max-height: none; +} +.logwrapper.expanded .logexpander:after { + content: "collapse [-]"; +} +.logwrapper .logexpander { + z-index: 1; + position: sticky; + top: 10px; + width: max-content; + border: 1px solid; + border-radius: 3px; + padding: 5px 7px; + margin: 10px 0 10px calc(100% - 80px); + cursor: pointer; + background-color: #e6e6e6; +} +.logwrapper .logexpander:after { + content: "expand [+]"; +} +.logwrapper .logexpander:hover { + color: #000; + border-color: #000; +} +.logwrapper .log { + min-height: 40px; + position: relative; + top: -50px; + height: calc(100% + 50px); + border: 1px solid #e6e6e6; + color: black; + display: block; + font-family: "Courier New", Courier, monospace; + padding: 5px; + padding-right: 80px; + white-space: pre-wrap; +} + +div.media { + border: 1px solid #e6e6e6; + float: right; + height: 240px; + margin: 0 5px; + overflow: hidden; + width: 320px; +} + +.media-container { + display: grid; + grid-template-columns: 25px auto 25px; + align-items: center; + flex: 1 1; + overflow: hidden; + height: 200px; +} + +.media-container--fullscreen { + grid-template-columns: 0px auto 0px; +} + +.media-container__nav--right, +.media-container__nav--left { + text-align: center; + cursor: pointer; +} + +.media-container__viewport { + cursor: pointer; + text-align: center; + height: inherit; +} +.media-container__viewport img, +.media-container__viewport video { + object-fit: cover; + width: 100%; + max-height: 100%; +} + +.media__name, +.media__counter { + display: flex; + flex-direction: row; + justify-content: space-around; + flex: 0 0 25px; + align-items: center; +} + +.collapsible td:not(.col-links) { + cursor: pointer; +} +.collapsible td:not(.col-links):hover::after { + color: #bbb; + font-style: italic; + cursor: pointer; +} + +.col-result { + width: 130px; +} +.col-result:hover::after { + content: " (hide details)"; +} + +.col-result.collapsed:hover::after { + content: " (show details)"; +} + +#environment-header h2:hover::after { + content: " (hide details)"; + color: #bbb; + font-style: italic; + cursor: pointer; + font-size: 12px; +} + +#environment-header.collapsed h2:hover::after { + content: " (show details)"; + color: #bbb; + font-style: italic; + cursor: pointer; + font-size: 12px; +} + +/*------------------ + * 3. Sorting items + *------------------*/ +.sortable { + cursor: pointer; +} +.sortable.desc:after { + content: " "; + position: relative; + left: 5px; + bottom: -12.5px; + border: 10px solid #4caf50; + border-bottom: 0; + border-left-color: transparent; + border-right-color: transparent; +} +.sortable.asc:after { + content: " "; + position: relative; + left: 5px; + bottom: 12.5px; + border: 10px solid #4caf50; + border-top: 0; + border-left-color: transparent; + border-right-color: transparent; +} + +.hidden, +.summary__reload__button.hidden { + display: none; +} + +.summary__data { + flex: 0 0 550px; +} +.summary__reload { + flex: 1 1; + display: flex; + justify-content: center; +} +.summary__reload__button { + flex: 0 0 300px; + display: flex; + color: white; + font-weight: bold; + background-color: #4caf50; + text-align: center; + justify-content: center; + align-items: center; + border-radius: 3px; + cursor: pointer; +} +.summary__reload__button:hover { + background-color: #46a049; +} +.summary__spacer { + flex: 0 0 550px; +} + +.controls { + display: flex; + justify-content: space-between; +} + +.filters, +.collapse { + display: flex; + align-items: center; +} +.filters button, +.collapse button { + color: #999; + border: none; + background: none; + cursor: pointer; + text-decoration: underline; +} +.filters button:hover, +.collapse button:hover { + color: #ccc; +} + +.filter__label { + margin-right: 10px; +} diff --git a/src/controllers/tests/report.html b/src/controllers/tests/report.html index 51753501..02c10e52 100644 --- a/src/controllers/tests/report.html +++ b/src/controllers/tests/report.html @@ -1,1373 +1,1373 @@ - - - - - report.html - - - -

report.html

-

- Report generated on 01-Aug-2024 at 10:30:20 by - pytest-html v4.1.1 -

-
-

Environment

-
-
- - - - - -
-
-

Summary

-
-

2 tests took 00:00:11.

-

(Un)check the boxes to filter the results.

-
- -
-
-
-
- - 0 Failed, - - 2 Passed, - - 0 Skipped, - - 0 Expected failures, - - 0 Unexpected passes, - - 0 Errors, - - 0 Reruns -
-
-  /  -
-
-
-
-
-
- - - - - - - - - -
ResultTestDurationLinks
- -
-
- -
- + + + + + report.html + + + +

report.html

+

+ Report generated on 01-Aug-2024 at 10:30:20 by + pytest-html v4.1.1 +

+
+

Environment

+
+
+ + + + + +
+
+

Summary

+
+

2 tests took 00:00:11.

+

(Un)check the boxes to filter the results.

+
+ +
+
+
+
+ + 0 Failed, + + 2 Passed, + + 0 Skipped, + + 0 Expected failures, + + 0 Unexpected passes, + + 0 Errors, + + 0 Reruns +
+
+  /  +
+
+
+
+
+
+ + + + + + + + + +
ResultTestDurationLinks
+ +
+
+ +
+ diff --git a/src/controllers/updateBlogController.ts b/src/controllers/updateBlogController.ts new file mode 100644 index 00000000..0c9cf320 --- /dev/null +++ b/src/controllers/updateBlogController.ts @@ -0,0 +1,30 @@ +import { Request, Response } from "express"; +import { updateBlogPost } from "../services/updateBlog.services"; + +export const updateBlogController = async (req: Request, res: Response) => { + const { id } = req.params; + const { title, content, published_at, image_url } = req.body; + + try { + const updatedBlog = await updateBlogPost( + id, + title, + content, + published_at, + image_url, + ); + + return res.status(200).json({ + status: "success", + status_code: 200, + message: "Blog post updated successfully", + data: updatedBlog, + }); + } catch (error) { + return res.status(500).json({ + status: "unsuccessful", + status_code: 500, + message: "Failed to update the blog post. Please try again later.", + }); + } +}; diff --git a/src/data-source.ts b/src/data-source.ts index c081a0f2..4dd0508e 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -1,34 +1,34 @@ -import "reflect-metadata"; -import { DataSource } from "typeorm"; -import config from "./config"; - -const isDevelopment = config.NODE_ENV === "development"; - -const AppDataSource = new DataSource({ - type: "postgres", - host: config.DB_HOST, - port: Number(config.DB_PORT) || 5432, - username: config.DB_USER, - password: config.DB_PASSWORD, - database: config.DB_NAME, - synchronize: isDevelopment, - logging: false, - entities: ["src/models/**/*.ts"], - migrations: ["src/migrations/**/*.ts"], - migrationsTableName: "migrations", - // ssl: true, - // extra: { - // ssl: { - // rejectUnauthorized: false, - // }, - // }, -}); - -export async function initializeDataSource() { - if (!AppDataSource.isInitialized) { - await AppDataSource.initialize(); - } - return AppDataSource; -} - -export default AppDataSource; +import "reflect-metadata"; +import { DataSource } from "typeorm"; +import config from "./config"; + +const isDevelopment = config.NODE_ENV === "development"; + +const AppDataSource = new DataSource({ + type: "postgres", + host: config.DB_HOST, + port: Number(config.DB_PORT) || 5432, + username: config.DB_USER, + password: config.DB_PASSWORD, + database: config.DB_NAME, + synchronize: isDevelopment, + logging: false, + entities: ["src/models/**/*.ts"], + migrations: ["src/migrations/**/*.ts"], + migrationsTableName: "migrations", + // ssl: true, + // extra: { + // ssl: { + // rejectUnauthorized: false, + // }, + // }, +}); + +export async function initializeDataSource() { + if (!AppDataSource.isInitialized) { + await AppDataSource.initialize(); + } + return AppDataSource; +} + +export default AppDataSource; diff --git a/src/enums/permission-category.enum.ts b/src/enums/permission-category.enum.ts index 27a37340..33943265 100644 --- a/src/enums/permission-category.enum.ts +++ b/src/enums/permission-category.enum.ts @@ -1,9 +1,9 @@ -export enum PermissionCategory { - CanViewTransactions = "canViewTransactions", - CanViewRefunds = "canViewRefunds", - CanLogRefunds = "canLogRefunds", - CanViewUsers = "canViewUsers", - CanCreateUsers = "canCreateUsers", - CanEditUsers = "canEditUsers", - CanBlacklistWhitelistUsers = "canBlacklistWhitelistUsers", -} +export enum PermissionCategory { + CanViewTransactions = "canViewTransactions", + CanViewRefunds = "canViewRefunds", + CanLogRefunds = "canLogRefunds", + CanViewUsers = "canViewUsers", + CanCreateUsers = "canCreateUsers", + CanEditUsers = "canEditUsers", + CanBlacklistWhitelistUsers = "canBlacklistWhitelistUsers", +} diff --git a/src/enums/product.ts b/src/enums/product.ts index 8ff94209..80015565 100644 --- a/src/enums/product.ts +++ b/src/enums/product.ts @@ -1,11 +1,11 @@ -export enum StockStatus { - IN_STOCK = "in stock", - OUT_STOCK = "out of stock", - LOW_STOCK = "low on stock", -} - -export enum ProductSize { - SMALL = "Small", - STANDARD = "Standard", - LARGE = "Large", -} +export enum StockStatus { + IN_STOCK = "in stock", + OUT_STOCK = "out of stock", + LOW_STOCK = "low on stock", +} + +export enum ProductSize { + SMALL = "Small", + STANDARD = "Standard", + LARGE = "Large", +} diff --git a/src/enums/userRoles.ts b/src/enums/userRoles.ts index 7298cff3..bd70e4b1 100644 --- a/src/enums/userRoles.ts +++ b/src/enums/userRoles.ts @@ -1,5 +1,5 @@ -export enum UserRole { - SUPER_ADMIN = 'super_admin', - ADMIN = 'admin', - USER = 'user', +export enum UserRole { + SUPER_ADMIN = 'super_admin', + ADMIN = 'admin', + USER = 'user', } \ No newline at end of file diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 31f21029..2160fdcc 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,54 +1,54 @@ -import { NextFunction, Request, Response } from "express"; -import jwt from "jsonwebtoken"; -import config from "../config"; -import { User } from "../models"; -import log from "../utils/logger"; -import { ServerError } from "./error"; - -export const authMiddleware = async ( - req: Request & { user?: User }, - res: Response, - next: NextFunction, -) => { - try { - const authHeader = req.headers.authorization; - - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return res.status(401).json({ - status_code: "401", - message: "Invalid token", - }); - } - - const token = authHeader.split(" ")[1]; - if (!token) { - return res.status(401).json({ - status_code: "401", - message: "Invalid token", - }); - } - - jwt.verify(token, config.TOKEN_SECRET, async (err, decoded: any) => { - if (err) { - return res.status(401).json({ - status_code: "401", - message: "Invalid token", - }); - } - const user = await User.findOne({ - where: { id: decoded["userId"] as string }, - }); - if (!user) { - return res.status(401).json({ - status_code: "401", - message: "Invalid token", - }); - } - req.user = user; - next(); - }); - } catch (error) { - log.error(error); - throw new ServerError("INTERNAL_SERVER_ERROR"); - } -}; +import { NextFunction, Request, Response } from "express"; +import jwt from "jsonwebtoken"; +import config from "../config"; +import { User } from "../models"; +import log from "../utils/logger"; +import { ServerError } from "./error"; + +export const authMiddleware = async ( + req: Request & { user?: User }, + res: Response, + next: NextFunction, +) => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ + status_code: "401", + message: "Invalid token", + }); + } + + const token = authHeader.split(" ")[1]; + if (!token) { + return res.status(401).json({ + status_code: "401", + message: "Invalid token", + }); + } + + jwt.verify(token, config.TOKEN_SECRET, async (err, decoded: any) => { + if (err) { + return res.status(401).json({ + status_code: "401", + message: "Invalid token", + }); + } + const user = await User.findOne({ + where: { id: decoded["userId"] as string }, + }); + if (!user) { + return res.status(401).json({ + status_code: "401", + message: "Invalid token", + }); + } + req.user = user; + next(); + }); + } catch (error) { + log.error(error); + throw new ServerError("INTERNAL_SERVER_ERROR"); + } +}; diff --git a/src/middleware/checkUserRole.ts b/src/middleware/checkUserRole.ts index 4c4d4627..b756a096 100644 --- a/src/middleware/checkUserRole.ts +++ b/src/middleware/checkUserRole.ts @@ -1,73 +1,74 @@ -import { Request, Response, NextFunction } from "express"; -import { UserRole } from "../enums/userRoles"; -import { HttpError, ServerError, Unauthorized } from "./error"; -import { User, UserOrganization } from "../models"; -import AppDataSource from "../data-source"; -import jwt from "jsonwebtoken"; - -export const checkPermissions = (roles: UserRole[]) => { - return async ( - req: Request & { user?: User }, - res: Response, - next: NextFunction, - ) => { - const authHeader = req.headers["authorization"]; - const token = authHeader && authHeader.split(" ")[1]; - try { - const decodedToken = jwt.decode(token); - if (typeof decodedToken === "string" || !decodedToken) { - return res - .status(401) - .json({ status: "error", message: "Access denied. Invalid token" }); - } - const userRepository = AppDataSource.getRepository(User); - const user = await userRepository.findOne({ - where: { id: decodedToken.userId }, - }); - - if (!user || !roles.includes(user.role)) { - return res - .status(403) - .json({ status: "error", message: "Access denied. Not an admin" }); - } - next(); - } catch (error) { - res - .status(401) - .json({ status: "error", message: "Access denied. Invalid token" }); - } - }; -}; - -export function adminOnly(req: Request, res: Response, next: NextFunction) { - const user = req.user; - - if (!user || user.role !== UserRole.ADMIN) { - return next(new Unauthorized("Access denied. Admins only.")); - } - next(); -} - -export const validOrgAdmin = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const user = req.user; - const { org_id } = req.params; - const userOrg = await AppDataSource.getRepository(UserOrganization).findOne( - { - where: { userId: user.id, organizationId: org_id }, - }, - ); - if (!userOrg || userOrg.role !== "admin") { - return next( - new Unauthorized("Access denied. Not an admin in this organization"), - ); - } - next(); - } catch (error) { - return new ServerError(error.message); - } -}; +import { Request, Response, NextFunction } from "express"; +import { UserRole } from "../enums/userRoles"; +import { HttpError, ServerError, Unauthorized } from "./error"; +import { User, UserOrganization } from "../models"; +import AppDataSource from "../data-source"; +import jwt from "jsonwebtoken"; + +export const checkPermissions = (roles: UserRole[]) => { + return async ( + req: Request & { user?: User }, + res: Response, + next: NextFunction, + ) => { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + try { + const decodedToken = jwt.decode(token); + if (typeof decodedToken === "string" || !decodedToken) { + return res + .status(401) + .json({ status: "error", message: "Access denied. Invalid token" }); + } + const userRepository = AppDataSource.getRepository(User); + const user = await userRepository.findOne({ + where: { id: decodedToken.userId }, + }); + + if (!user || !roles.includes(user.role)) { + return res + .status(403) + .json({ status: "error", message: "Access denied. Not an admin" }); + } + next(); + } catch (error) { + res + .status(401) + .json({ status: "error", message: "Access denied. Invalid token" }); + } + }; +}; + +export function adminOnly(req: Request, res: Response, next: NextFunction) { + const user = req.user; + + if (!user || user.role !== UserRole.ADMIN) { + return next(new Unauthorized("Access denied. Admins only.")); + } + + next(); +} + +export const validOrgAdmin = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const user = req.user; + const { org_id } = req.params; + const userOrg = await AppDataSource.getRepository(UserOrganization).findOne( + { + where: { userId: user.id, organizationId: org_id }, + }, + ); + if (!userOrg || userOrg.role !== "admin") { + return next( + new Unauthorized("Access denied. Not an admin in this organization"), + ); + } + next(); + } catch (error) { + return new ServerError(error.message); + } +}; diff --git a/src/middleware/error.ts b/src/middleware/error.ts index 32ce4a4f..2372afc5 100644 --- a/src/middleware/error.ts +++ b/src/middleware/error.ts @@ -1,87 +1,87 @@ -import { NextFunction, Request, Response } from "express"; - -class HttpError extends Error { - status_code: number; - success: boolean = false; - - constructor(statusCode: number, message: string) { - super(message); - this.name = this.constructor.name; - this.status_code = statusCode; - } -} - -class BadRequest extends HttpError { - constructor(message: string) { - super(400, message); - } -} - -class ResourceNotFound extends HttpError { - constructor(message: string) { - super(404, message); - } -} - -class Unauthorized extends HttpError { - constructor(message: string) { - super(401, message); - } -} - -class Forbidden extends HttpError { - constructor(message: string) { - super(403, message); - } -} - -class Conflict extends HttpError { - constructor(message: string) { - super(409, message); - } -} - -class InvalidInput extends HttpError { - constructor(message: string) { - super(422, message); - } -} - -class ServerError extends HttpError { - constructor(message: string) { - super(500, message); - } -} - -const routeNotFound = (req: Request, res: Response, next: NextFunction) => { - const message = `Route not found: ${req.originalUrl}`; - res.status(404).json({ success: false, status: 404, message }); -}; - -const errorHandler = ( - err: HttpError, - _req: Request, - res: Response, - _next: NextFunction, -) => { - const { success, status_code, message } = err; - const cleanedMessage = message.replace(/"/g, ""); - res.status(status_code).json({ - success: success || "unsuccessful", - status_code, - message: cleanedMessage, - }); -}; - -export { - BadRequest, - Conflict, - errorHandler, - Forbidden, - HttpError, - InvalidInput, - ResourceNotFound, - routeNotFound, - ServerError, - Unauthorized, -}; +import { NextFunction, Request, Response } from "express"; + +class HttpError extends Error { + status_code: number; + success: boolean = false; + + constructor(statusCode: number, message: string) { + super(message); + this.name = this.constructor.name; + this.status_code = statusCode; + } +} + +class BadRequest extends HttpError { + constructor(message: string) { + super(400, message); + } +} + +class ResourceNotFound extends HttpError { + constructor(message: string) { + super(404, message); + } +} + +class Unauthorized extends HttpError { + constructor(message: string) { + super(401, message); + } +} + +class Forbidden extends HttpError { + constructor(message: string) { + super(403, message); + } +} + +class Conflict extends HttpError { + constructor(message: string) { + super(409, message); + } +} + +class InvalidInput extends HttpError { + constructor(message: string) { + super(422, message); + } +} + +class ServerError extends HttpError { + constructor(message: string) { + super(500, message); + } +} + +const routeNotFound = (req: Request, res: Response, next: NextFunction) => { + const message = `Route not found: ${req.originalUrl}`; + res.status(404).json({ success: false, status: 404, message }); +}; + +const errorHandler = ( + err: HttpError, + _req: Request, + res: Response, + _next: NextFunction, +) => { + const { success, status_code, message } = err; + const cleanedMessage = message.replace(/"/g, ""); + res.status(status_code).json({ + success: success || "unsuccessful", + status_code, + message: cleanedMessage, + }); +}; + +export { + BadRequest, + Conflict, + errorHandler, + Forbidden, + HttpError, + InvalidInput, + ResourceNotFound, + routeNotFound, + ServerError, + Unauthorized, +}; diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 44396361..c70e3eb2 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -1,4 +1,4 @@ -export * from "./error"; -export * from "./auth"; -export * from "./checkUserRole"; -export * from "./organizationValidation"; +export * from "./error"; +export * from "./auth"; +export * from "./checkUserRole"; +export * from "./organizationValidation"; diff --git a/src/middleware/organizationValidation.ts b/src/middleware/organizationValidation.ts index c2273855..fc45bccf 100644 --- a/src/middleware/organizationValidation.ts +++ b/src/middleware/organizationValidation.ts @@ -1,242 +1,255 @@ -import { NextFunction, Request, Response } from "express"; -import { param, validationResult, body } from "express-validator"; -import { z } from "zod"; -import { User } from "../models"; -import { OrgService } from "../services/org.services"; -import log from "../utils/logger"; -import { InvalidInput } from "./error"; - -export const organizationValidation = async ( - req: Request & { user?: User }, - res: Response, - next: NextFunction, -) => { - try { - const organisationSchema = z.object({ - name: z.string({ - required_error: "name is required", - invalid_type_error: "name must be a string", - }), - description: z.string({ - required_error: "description is required", - invalid_type_error: "description must be a string", - }), - email: z.string({ - required_error: "email is required", - invalid_type_error: "description must be a string", - }), - industry: z.string({ - required_error: "industry is required", - invalid_type_error: "description must be a string", - }), - type: z.string({ - required_error: "type is required", - invalid_type_error: "description must be a string", - }), - country: z.string({ - required_error: "country is required", - invalid_type_error: "description must be a string", - }), - address: z.string({ - required_error: "address is required", - invalid_type_error: "description must be a string", - }), - state: z.string({ - required_error: "state is required", - invalid_type_error: "description must be a string", - }), - }); - organisationSchema.parse(req.body); - - next(); - } catch (error) { - const validationErrors = error.issues.map((issue) => { - return { field: issue.path[0], message: issue.message }; - }); - res.statusCode = 422; - return res.json({ errors: validationErrors }).status(422); - } -}; - -try { -} catch (error) {} - -export const validateOrgId = [ - param("org_id") - .notEmpty() - .withMessage("Organisation id is required") - .isString() - .withMessage("Organisation id must be a string") - .isUUID() - .withMessage("Valid org_id must be provided") - .trim() - .escape(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - throw new InvalidInput( - `${errors - .array() - .map((error) => error.msg) - .join(", ")}`, - ); - } - next(); - }, -]; - -export const validateUpdateOrg = [ - param("organization_id") - .notEmpty() - .withMessage("Organisation id is required") - .isString() - .withMessage("Organisation id must be a string") - .isUUID() - .withMessage("Valid organization ID must be provided") - .trim() - .escape(), - body("name") - .notEmpty() - .withMessage("Name is required") - .isString() - .withMessage("Name must be a string") - .trim() - .escape(), - body("email") - .notEmpty() - .withMessage("Email is required") - .isEmail() - .withMessage("Valid email must be provided") - .trim() - .escape(), - body("industry") - .notEmpty() - .withMessage("Industry is required") - .isString() - .withMessage("Industry must be a string") - .trim() - .escape(), - body("type") - .notEmpty() - .withMessage("Type is required") - .isString() - .withMessage("Type must be a string") - .trim() - .escape(), - body("country") - .notEmpty() - .withMessage("Country is required") - .isString() - .withMessage("Country must be a string") - .trim() - .escape(), - body("address") - .notEmpty() - .withMessage("Address is required") - .isString() - .withMessage("Address must be a string") - .trim() - .escape(), - body("state") - .notEmpty() - .withMessage("State is required") - .isString() - .withMessage("State must be a string") - .trim() - .escape(), - body("description") - .notEmpty() - .withMessage("Description is required") - .isString() - .withMessage("Description must be a string") - .trim() - .escape(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(422).json({ - status: "Error", - status_code: 422, - message: - "Valid organization ID, name, email, industry, type, country, address, state, and description must be provided.", - }); - } - next(); - }, -]; - -export const validateUserToOrg = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const { org_id } = req.params; - const { user } = req; - - if (!user || !org_id) { - return res.status(400).json({ - status_code: 400, - message: "user or organization id is missing", - }); - } - - const orgService = new OrgService(); - const userOrg = await orgService.getSingleOrg(org_id, user.id); - - if (!userOrg) { - return res.status(400).json({ - status_code: 400, - message: "user not a member of organization", - }); - } - - next(); - } catch (error) { - res.status(500).json({ - status_code: 500, - message: "Internal server error", - }); - } -}; - -export const validateOrgRole = [ - param("org_id") - .notEmpty() - .withMessage("Organisation id is required") - .isString() - .withMessage("Organisation id must be a string") - .isUUID() - .withMessage("Valid organization ID must be provided") - .trim() - .escape(), - body("name") - .notEmpty() - .withMessage("Name is required") - .isString() - .isLength({ max: 50 }) - .withMessage("Name must be a string") - .trim() - .escape(), - body("description") - .optional() - .isString() - .isLength({ max: 200 }) - .withMessage("Description must be a string") - .trim() - .escape(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(422).json({ - status: "Error", - status_code: 422, - message: - "Valid organization ID, name, and description must be provided.", - }); - } - next(); - }, -]; - -//TODO: Add validation for update organization +import { NextFunction, Request, Response } from "express"; +import { param, validationResult, body } from "express-validator"; +import { z } from "zod"; +import { User } from "../models"; +import { OrgService } from "../services/org.services"; +import log from "../utils/logger"; +import { InvalidInput } from "./error"; +import { UserOrganization } from "../models"; +import { UserRole } from "../enums/userRoles"; + +export const organizationValidation = async ( + req: Request & { user?: User }, + res: Response, + next: NextFunction, +) => { + try { + const organisationSchema = z.object({ + name: z.string({ + required_error: "name is required", + invalid_type_error: "name must be a string", + }), + description: z.string({ + required_error: "description is required", + invalid_type_error: "description must be a string", + }), + email: z.string({ + required_error: "email is required", + invalid_type_error: "description must be a string", + }), + industry: z.string({ + required_error: "industry is required", + invalid_type_error: "description must be a string", + }), + type: z.string({ + required_error: "type is required", + invalid_type_error: "description must be a string", + }), + country: z.string({ + required_error: "country is required", + invalid_type_error: "description must be a string", + }), + address: z.string({ + required_error: "address is required", + invalid_type_error: "description must be a string", + }), + state: z.string({ + required_error: "state is required", + invalid_type_error: "description must be a string", + }), + }); + organisationSchema.parse(req.body); + + next(); + } catch (error) { + const validationErrors = error.issues.map((issue) => { + return { field: issue.path[0], message: issue.message }; + }); + res.statusCode = 422; + return res.json({ errors: validationErrors }).status(422); + } +}; + +try { +} catch (error) {} + +export const validateOrgId = [ + param("org_id") + .notEmpty() + .withMessage("Organisation id is required") + .isString() + .withMessage("Organisation id must be a string") + .isUUID() + .withMessage("Valid org_id must be provided") + .trim() + .escape(), + (req: Request, res: Response, next: NextFunction) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + throw new InvalidInput( + `${errors + .array() + .map((error) => error.msg) + .join(", ")}`, + ); + } + next(); + }, +]; + +export const validateUpdateOrg = [ + param("organization_id") + .notEmpty() + .withMessage("Organisation id is required") + .isString() + .withMessage("Organisation id must be a string") + .isUUID() + .withMessage("Valid organization ID must be provided") + .trim() + .escape(), + body("name") + .notEmpty() + .withMessage("Name is required") + .isString() + .withMessage("Name must be a string") + .trim() + .escape(), + body("email") + .notEmpty() + .withMessage("Email is required") + .isEmail() + .withMessage("Valid email must be provided") + .trim() + .escape(), + body("industry") + .notEmpty() + .withMessage("Industry is required") + .isString() + .withMessage("Industry must be a string") + .trim() + .escape(), + body("type") + .notEmpty() + .withMessage("Type is required") + .isString() + .withMessage("Type must be a string") + .trim() + .escape(), + body("country") + .notEmpty() + .withMessage("Country is required") + .isString() + .withMessage("Country must be a string") + .trim() + .escape(), + body("address") + .notEmpty() + .withMessage("Address is required") + .isString() + .withMessage("Address must be a string") + .trim() + .escape(), + body("state") + .notEmpty() + .withMessage("State is required") + .isString() + .withMessage("State must be a string") + .trim() + .escape(), + body("description") + .notEmpty() + .withMessage("Description is required") + .isString() + .withMessage("Description must be a string") + .trim() + .escape(), + (req: Request, res: Response, next: NextFunction) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(422).json({ + status: "Error", + status_code: 422, + message: + "Valid organization ID, name, email, industry, type, country, address, state, and description must be provided.", + }); + } + next(); + }, +]; + +export const validateUserToOrg = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { org_id } = req.params; + const { user } = req; + + if (!user || !org_id) { + return res.status(400).json({ + status_code: 400, + message: "user or organization id is missing", + }); + } + + const orgMember = await UserOrganization.findOne({ + where: { + userId: user.id, + organizationId: org_id, + }, + }); + + if (!orgMember) { + return res.status(400).json({ + status_code: 400, + message: "user not a member of organization", + }); + } + + if (![UserRole.ADMIN, UserRole.SUPER_ADMIN].includes(orgMember.role)) { + return res.status(400).json({ + status_code: 403, + message: "Forbidden: User not an admin or super admin", + }); + } + + next(); + } catch (error) { + res.status(500).json({ + status_code: 500, + message: "Internal server error", + }); + } +}; + +export const validateOrgRole = [ + param("org_id") + .notEmpty() + .withMessage("Organisation id is required") + .isString() + .withMessage("Organisation id must be a string") + .isUUID() + .withMessage("Valid organization ID must be provided") + .trim() + .escape(), + body("name") + .notEmpty() + .withMessage("Name is required") + .isString() + .isLength({ max: 50 }) + .withMessage("Name must be a string") + .trim() + .escape(), + body("description") + .optional() + .isString() + .isLength({ max: 200 }) + .withMessage("Description must be a string") + .trim() + .escape(), + (req: Request, res: Response, next: NextFunction) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(422).json({ + status: "Error", + status_code: 422, + message: + "Valid organization ID, name, and description must be provided.", + }); + } + next(); + }, +]; + +//TODO: Add validation for update organization diff --git a/src/middleware/paymentStripeValidation.ts b/src/middleware/paymentStripeValidation.ts index 86355927..7c843550 100644 --- a/src/middleware/paymentStripeValidation.ts +++ b/src/middleware/paymentStripeValidation.ts @@ -1,46 +1,46 @@ -import { Request, Response, NextFunction } from "express"; -import { body, validationResult } from "express-validator"; - -/** - * Validation rules for the payment request using Stripe - */ -export const validatePaymentRequest = [ - body("payer_type") - .notEmpty() - .withMessage("payer_type is required") - .isIn(["user", "organization"]) - .withMessage('payer_type must be either "user" or "organization"'), - - body("payer_id") - .notEmpty() - .withMessage("payer_id is required") - .isEmail() - .withMessage("payer_id must be a valid email address"), - - body("amount") - .notEmpty() - .withMessage("amount is required") - .isFloat({ gt: 0 }) - .withMessage("amount must be a positive number"), - - body("currency") - .notEmpty() - .withMessage("currency is required") - .isIn(["USD", "EUR", "KSH", "Naira"]) - .withMessage('currency must be either "USD", "EUR", "KSH", or "Naira"'), - - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ - error: "Bad Request", - message: errors - .array() - .map((err) => err.msg) - .join(", "), - status_code: 400, - }); - } - next(); - }, -]; +import { Request, Response, NextFunction } from "express"; +import { body, validationResult } from "express-validator"; + +/** + * Validation rules for the payment request using Stripe + */ +export const validatePaymentRequest = [ + body("payer_type") + .notEmpty() + .withMessage("payer_type is required") + .isIn(["user", "organization"]) + .withMessage('payer_type must be either "user" or "organization"'), + + body("payer_id") + .notEmpty() + .withMessage("payer_id is required") + .isEmail() + .withMessage("payer_id must be a valid email address"), + + body("amount") + .notEmpty() + .withMessage("amount is required") + .isFloat({ gt: 0 }) + .withMessage("amount must be a positive number"), + + body("currency") + .notEmpty() + .withMessage("currency is required") + .isIn(["USD", "EUR", "KSH", "Naira"]) + .withMessage('currency must be either "USD", "EUR", "KSH", or "Naira"'), + + (req: Request, res: Response, next: NextFunction) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + error: "Bad Request", + message: errors + .array() + .map((err) => err.msg) + .join(", "), + status_code: 400, + }); + } + next(); + }, +]; diff --git a/src/middleware/product.ts b/src/middleware/product.ts index 5028b741..c7e585b6 100644 --- a/src/middleware/product.ts +++ b/src/middleware/product.ts @@ -1,16 +1,16 @@ -import { Request, Response, NextFunction } from "express"; - -import { body, validationResult } from "express-validator"; -// Middleware to validate and sanitize product details -export const validateProductDetails = [ - body("name").trim().escape(), - body("description").trim().escape(), - body("category").trim().escape(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - next(); - }, -]; +import { Request, Response, NextFunction } from "express"; + +import { body, validationResult } from "express-validator"; +// Middleware to validate and sanitize product details +export const validateProductDetails = [ + body("name").trim().escape(), + body("description").trim().escape(), + body("category").trim().escape(), + (req: Request, res: Response, next: NextFunction) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + next(); + }, +]; diff --git a/src/middleware/request-validation.ts b/src/middleware/request-validation.ts index 57812ad8..0ed43500 100644 --- a/src/middleware/request-validation.ts +++ b/src/middleware/request-validation.ts @@ -1,21 +1,21 @@ -import { NextFunction, Request, Response } from "express"; -import { ZodError, ZodSchema } from "zod"; - -const requestBodyValidator = (schema: ZodSchema) => { - return (req: Request, res: Response, next: NextFunction) => { - try { - schema.parse(req.body); - next(); - } catch (err) { - if (err instanceof ZodError) { - return res.status(400).json({ - message: "Invalid request body", - // errors: err.errors, - }); - } - next(err); - } - }; -}; - -export { requestBodyValidator }; +import { NextFunction, Request, Response } from "express"; +import { ZodError, ZodSchema } from "zod"; + +const requestBodyValidator = (schema: ZodSchema) => { + return (req: Request, res: Response, next: NextFunction) => { + try { + schema.parse(req.body); + next(); + } catch (err) { + if (err instanceof ZodError) { + return res.status(400).json({ + message: "Invalid request body", + // errors: err.errors, + }); + } + next(err); + } + }; +}; + +export { requestBodyValidator }; diff --git a/src/middleware/testimonial.validation.ts b/src/middleware/testimonial.validation.ts index 1e23955a..bb4d78fe 100644 --- a/src/middleware/testimonial.validation.ts +++ b/src/middleware/testimonial.validation.ts @@ -1,17 +1,17 @@ -import { body, validationResult } from "express-validator"; -import { Request, Response, NextFunction } from "express"; -import { InvalidInput } from "./error"; - -export const validateTestimonial = [ - body("client_name").notEmpty().withMessage("Client name is required"), - body("client_position").notEmpty().withMessage("Client position is required"), - body("testimonial").notEmpty().withMessage("Testimonial is required"), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - - if (!errors.isEmpty()) { - throw new InvalidInput("Validation failed"); - } - next(); - }, -]; +import { body, validationResult } from "express-validator"; +import { Request, Response, NextFunction } from "express"; +import { InvalidInput } from "./error"; + +export const validateTestimonial = [ + body("client_name").notEmpty().withMessage("Client name is required"), + body("client_position").notEmpty().withMessage("Client position is required"), + body("testimonial").notEmpty().withMessage("Testimonial is required"), + (req: Request, res: Response, next: NextFunction) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + throw new InvalidInput("Validation failed"); + } + next(); + }, +]; diff --git a/src/migrations/1723120636893-CreateUsersTable.ts b/src/migrations/1723120636893-CreateUsersTable.ts index b2879ee9..edd335a9 100644 --- a/src/migrations/1723120636893-CreateUsersTable.ts +++ b/src/migrations/1723120636893-CreateUsersTable.ts @@ -1,331 +1,331 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class CreateUsersTable1723120636893 implements MigrationInterface { - name = "CreateUsersTable1723120636893"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "category" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_9c4e4a89e3674fc9f382d733f03" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "comment" ("id" SERIAL NOT NULL, "content" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "blogId" uuid, CONSTRAINT "PK_0b0e4bbc8415ec426f87f3a88e2" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "payment" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "billingPlanId" uuid, "amount" numeric(10,2) NOT NULL, "currency" character varying NOT NULL, "paymentServiceId" character varying, "status" "public"."payment_status_enum" NOT NULL, "provider" "public"."payment_provider_enum" NOT NULL, "organizationId" uuid, "description" character varying, "metadata" jsonb, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_fcaec7df5adf9cac408c686b2ab" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "billing_plan" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "organizationId" uuid NOT NULL, "name" character varying NOT NULL, "price" numeric(10,2) NOT NULL, "currency" character varying NOT NULL, "duration" character varying NOT NULL, "description" character varying, "features" text NOT NULL, CONSTRAINT "PK_63f4db8ca9063690ab4dfc3b3da" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "permissions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "category" "public"."permissions_category_enum" NOT NULL, "permission_list" boolean NOT NULL, "roleId" uuid, CONSTRAINT "PK_920331560282b8bd21bb02290df" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "roles" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(50) NOT NULL, "description" character varying(200), "organizationId" uuid, CONSTRAINT "PK_c1433d71a4838793a49dcad46ab" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "profile" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "first_name" character varying NOT NULL, "last_name" character varying NOT NULL, "phone_number" character varying, "avatarUrl" character varying NOT NULL, CONSTRAINT "PK_3dd8bfc97e4a77c70971591bdcb" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "organization_member" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userIdId" uuid, "organizationIdId" uuid, "roleId" uuid, "profileIdId" uuid, CONSTRAINT "PK_81dbbb093cbe0539c170f3d1484" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "product" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "description" character varying NOT NULL, "price" integer NOT NULL, "quantity" integer NOT NULL DEFAULT '1', "category" character varying NOT NULL, "image" character varying NOT NULL, "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "size" "public"."product_size_enum" NOT NULL DEFAULT 'Standard', "stock_status" "public"."product_stock_status_enum" NOT NULL DEFAULT 'out of stock', "orgId" uuid, "userId" uuid, CONSTRAINT "PK_bebc9158e480b949565b4dc7a82" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "user_organization" ("userId" uuid NOT NULL, "organizationId" uuid NOT NULL, "role" "public"."user_organization_role_enum" NOT NULL, CONSTRAINT "PK_6e6630567770ae6f0a76d05ce33" PRIMARY KEY ("userId", "organizationId"))`, - ); - await queryRunner.query( - `CREATE TABLE "organization" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "slug" character varying NOT NULL, "name" character varying NOT NULL, "email" character varying, "industry" character varying, "type" character varying, "country" character varying, "address" character varying, "state" character varying, "description" text, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "owner_id" uuid NOT NULL, CONSTRAINT "UQ_a08804baa7c5d5427067c49a31f" UNIQUE ("slug"), CONSTRAINT "PK_472c1f99a32def1b0abb219cd67" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "help_center_topic" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying NOT NULL, "content" character varying NOT NULL, "author" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_f1fd49531d0c8c8ecf09fca6e84" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "notification_setting" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying NOT NULL, "email_notifications" boolean NOT NULL, "push_notifications" boolean NOT NULL, "sms_notifications" boolean NOT NULL, CONSTRAINT "UQ_d210b9143572b7e8179c15f5f2a" UNIQUE ("user_id"), CONSTRAINT "PK_af85fd153b97ee9eacb505453fe" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "sms" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "phone_number" character varying NOT NULL, "message" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "senderId" uuid, CONSTRAINT "PK_60793c2f16aafe0513f8817eae8" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying NOT NULL, "user_id" character varying NOT NULL, "description" character varying NOT NULL, "location" character varying NOT NULL, "salary" character varying NOT NULL, "job_type" character varying NOT NULL, "company_name" character varying NOT NULL, CONSTRAINT "PK_98ab1c14ff8d1cf80d18703b92f" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "email_queue" ("id" SERIAL NOT NULL, "templateId" character varying NOT NULL, "recipient" character varying NOT NULL, "variables" json NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_b6c031a57087af131ed0176e17c" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "log" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying NOT NULL, "action" character varying NOT NULL, "details" character varying NOT NULL, "timestamp" TIMESTAMP NOT NULL, CONSTRAINT "PK_350604cbdf991d5930d9e618fbd" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "org_invite_token" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying NOT NULL, "expires_at" TIMESTAMP NOT NULL, "isActivated" boolean NOT NULL DEFAULT true, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "organizationId" uuid, CONSTRAINT "PK_7d3d1855ecf3e58dc28eb655afa" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "invitation" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying NOT NULL, "email" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "organizationId" uuid, "orgInviteTokenId" uuid, CONSTRAINT "PK_beb994737756c0f18a1c1f8669c" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "contact" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(100) NOT NULL, "email" character varying(100) NOT NULL, "phoneNumber" character varying(20) NOT NULL, "message" text NOT NULL, CONSTRAINT "PK_2cbbe00f59ab6b3bb5b8d19f989" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "faq" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "question" character varying NOT NULL, "answer" character varying NOT NULL, "category" character varying NOT NULL, "createdBy" character varying NOT NULL DEFAULT 'super_admin', CONSTRAINT "PK_d6f5a52b1a96dd8d0591f9fbc47" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "email" character varying NOT NULL, "password" character varying, "google_id" character varying, "isverified" boolean NOT NULL DEFAULT false, "role" "public"."user_role_enum" NOT NULL DEFAULT 'user', "otp" integer, "otp_expires_at" TIMESTAMP, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "is_deleted" boolean NOT NULL DEFAULT false, "deletedAt" TIMESTAMP, "passwordResetToken" character varying, "passwordResetExpires" bigint, "timezone" jsonb, "profileId" uuid, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "REL_9466682df91534dd95e4dbaa61" UNIQUE ("profileId"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "like" ("id" SERIAL NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "blogId" uuid, "userId" uuid, CONSTRAINT "PK_eff3e46d24d416b52a7e0ae4159" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "blog" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying NOT NULL, "content" character varying NOT NULL, "image_url" character varying, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "published_at" TIMESTAMP, "authorId" uuid, CONSTRAINT "PK_85c6532ad065a448e9de7638571" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "tag" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_8e4052373c579afc1471f526760" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "testimonial" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying NOT NULL, "client_name" character varying NOT NULL, "client_position" character varying NOT NULL, "testimonial" character varying NOT NULL, CONSTRAINT "PK_e1aee1c726db2d336480c69f7cb" PRIMARY KEY ("id"))`, - ); - await queryRunner.query( - `CREATE TABLE "user_organizations_organization" ("userId" uuid NOT NULL, "organizationId" uuid NOT NULL, CONSTRAINT "PK_d89fbba617c90c71e2fc0bee26f" PRIMARY KEY ("userId", "organizationId"))`, - ); - await queryRunner.query( - `CREATE INDEX "IDX_7ad3d8541fbdb5a3d137c50fb4" ON "user_organizations_organization" ("userId") `, - ); - await queryRunner.query( - `CREATE INDEX "IDX_8d7c566d5a234be0a646101326" ON "user_organizations_organization" ("organizationId") `, - ); - await queryRunner.query( - `CREATE TABLE "blog_tags_tag" ("blogId" uuid NOT NULL, "tagId" integer NOT NULL, CONSTRAINT "PK_163bef1f79bd1f15b07f75e072d" PRIMARY KEY ("blogId", "tagId"))`, - ); - await queryRunner.query( - `CREATE INDEX "IDX_9572d27777384d535f77ed780d" ON "blog_tags_tag" ("blogId") `, - ); - await queryRunner.query( - `CREATE INDEX "IDX_066934a149d9efba507443ce88" ON "blog_tags_tag" ("tagId") `, - ); - await queryRunner.query( - `CREATE TABLE "blog_categories_category" ("blogId" uuid NOT NULL, "categoryId" integer NOT NULL, CONSTRAINT "PK_5f83120a485466f9e3fe7ada496" PRIMARY KEY ("blogId", "categoryId"))`, - ); - await queryRunner.query( - `CREATE INDEX "IDX_200fdd3b43e7dfde885cf71bd3" ON "blog_categories_category" ("blogId") `, - ); - await queryRunner.query( - `CREATE INDEX "IDX_f5665dcbec4177775b6edab2b9" ON "blog_categories_category" ("categoryId") `, - ); - await queryRunner.query( - `ALTER TABLE "comment" ADD CONSTRAINT "FK_5dec255234c5b7418f3d1e88ce4" FOREIGN KEY ("blogId") REFERENCES "blog"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "payment" ADD CONSTRAINT "FK_be7fcc9fb8cd5a74cb602ec6c9b" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "payment" ADD CONSTRAINT "FK_6187e7a1d8072420d58d5340b09" FOREIGN KEY ("billingPlanId") REFERENCES "billing_plan"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "billing_plan" ADD CONSTRAINT "FK_e5f604154fb0c0cb99c9739fc69" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "permissions" ADD CONSTRAINT "FK_36d7b8e1a331102ec9161e879ce" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "roles" ADD CONSTRAINT "FK_0933e1dfb2993d672af1a98f08e" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "organization_member" ADD CONSTRAINT "FK_b7b4b3aecc1aad541c29db28923" FOREIGN KEY ("userIdId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "organization_member" ADD CONSTRAINT "FK_5ed6a30682214b2782545b389ee" FOREIGN KEY ("organizationIdId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "organization_member" ADD CONSTRAINT "FK_6f28dff88284c106b92c84d8625" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "organization_member" ADD CONSTRAINT "FK_72d91423c27a90fd9f56925b30a" FOREIGN KEY ("profileIdId") REFERENCES "profile"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "product" ADD CONSTRAINT "FK_4001796e6dec57fa1424e6ffe22" FOREIGN KEY ("orgId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "product" ADD CONSTRAINT "FK_329b8ae12068b23da547d3b4798" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "user_organization" ADD CONSTRAINT "FK_29c3c8cc3ea9db22e4a347f4b5a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "user_organization" ADD CONSTRAINT "FK_7143f31467178a6164a42426c15" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "sms" ADD CONSTRAINT "FK_5e4a3ebde193729147d95e0822c" FOREIGN KEY ("senderId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "org_invite_token" ADD CONSTRAINT "FK_4775270ba8c3a34c05ce4ec9951" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "invitation" ADD CONSTRAINT "FK_5c00d7d515395f91bd1fee19f32" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "invitation" ADD CONSTRAINT "FK_043292be3660aa8e5a46de7c4d7" FOREIGN KEY ("orgInviteTokenId") REFERENCES "org_invite_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "user" ADD CONSTRAINT "FK_9466682df91534dd95e4dbaa616" FOREIGN KEY ("profileId") REFERENCES "profile"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "like" ADD CONSTRAINT "FK_1b343f6df7583577dffcd777120" FOREIGN KEY ("blogId") REFERENCES "blog"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "like" ADD CONSTRAINT "FK_e8fb739f08d47955a39850fac23" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "blog" ADD CONSTRAINT "FK_a001483d5ba65dad16557cd6ddb" FOREIGN KEY ("authorId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "user_organizations_organization" ADD CONSTRAINT "FK_7ad3d8541fbdb5a3d137c50fb40" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ); - await queryRunner.query( - `ALTER TABLE "user_organizations_organization" ADD CONSTRAINT "FK_8d7c566d5a234be0a6461013269" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "blog_tags_tag" ADD CONSTRAINT "FK_9572d27777384d535f77ed780d0" FOREIGN KEY ("blogId") REFERENCES "blog"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ); - await queryRunner.query( - `ALTER TABLE "blog_tags_tag" ADD CONSTRAINT "FK_066934a149d9efba507443ce889" FOREIGN KEY ("tagId") REFERENCES "tag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "blog_categories_category" ADD CONSTRAINT "FK_200fdd3b43e7dfde885cf71bd3a" FOREIGN KEY ("blogId") REFERENCES "blog"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ); - await queryRunner.query( - `ALTER TABLE "blog_categories_category" ADD CONSTRAINT "FK_f5665dcbec4177775b6edab2b9b" FOREIGN KEY ("categoryId") REFERENCES "category"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "blog_categories_category" DROP CONSTRAINT "FK_f5665dcbec4177775b6edab2b9b"`, - ); - await queryRunner.query( - `ALTER TABLE "blog_categories_category" DROP CONSTRAINT "FK_200fdd3b43e7dfde885cf71bd3a"`, - ); - await queryRunner.query( - `ALTER TABLE "blog_tags_tag" DROP CONSTRAINT "FK_066934a149d9efba507443ce889"`, - ); - await queryRunner.query( - `ALTER TABLE "blog_tags_tag" DROP CONSTRAINT "FK_9572d27777384d535f77ed780d0"`, - ); - await queryRunner.query( - `ALTER TABLE "user_organizations_organization" DROP CONSTRAINT "FK_8d7c566d5a234be0a6461013269"`, - ); - await queryRunner.query( - `ALTER TABLE "user_organizations_organization" DROP CONSTRAINT "FK_7ad3d8541fbdb5a3d137c50fb40"`, - ); - await queryRunner.query( - `ALTER TABLE "blog" DROP CONSTRAINT "FK_a001483d5ba65dad16557cd6ddb"`, - ); - await queryRunner.query( - `ALTER TABLE "like" DROP CONSTRAINT "FK_e8fb739f08d47955a39850fac23"`, - ); - await queryRunner.query( - `ALTER TABLE "like" DROP CONSTRAINT "FK_1b343f6df7583577dffcd777120"`, - ); - await queryRunner.query( - `ALTER TABLE "user" DROP CONSTRAINT "FK_9466682df91534dd95e4dbaa616"`, - ); - await queryRunner.query( - `ALTER TABLE "invitation" DROP CONSTRAINT "FK_043292be3660aa8e5a46de7c4d7"`, - ); - await queryRunner.query( - `ALTER TABLE "invitation" DROP CONSTRAINT "FK_5c00d7d515395f91bd1fee19f32"`, - ); - await queryRunner.query( - `ALTER TABLE "org_invite_token" DROP CONSTRAINT "FK_4775270ba8c3a34c05ce4ec9951"`, - ); - await queryRunner.query( - `ALTER TABLE "sms" DROP CONSTRAINT "FK_5e4a3ebde193729147d95e0822c"`, - ); - await queryRunner.query( - `ALTER TABLE "user_organization" DROP CONSTRAINT "FK_7143f31467178a6164a42426c15"`, - ); - await queryRunner.query( - `ALTER TABLE "user_organization" DROP CONSTRAINT "FK_29c3c8cc3ea9db22e4a347f4b5a"`, - ); - await queryRunner.query( - `ALTER TABLE "product" DROP CONSTRAINT "FK_329b8ae12068b23da547d3b4798"`, - ); - await queryRunner.query( - `ALTER TABLE "product" DROP CONSTRAINT "FK_4001796e6dec57fa1424e6ffe22"`, - ); - await queryRunner.query( - `ALTER TABLE "organization_member" DROP CONSTRAINT "FK_72d91423c27a90fd9f56925b30a"`, - ); - await queryRunner.query( - `ALTER TABLE "organization_member" DROP CONSTRAINT "FK_6f28dff88284c106b92c84d8625"`, - ); - await queryRunner.query( - `ALTER TABLE "organization_member" DROP CONSTRAINT "FK_5ed6a30682214b2782545b389ee"`, - ); - await queryRunner.query( - `ALTER TABLE "organization_member" DROP CONSTRAINT "FK_b7b4b3aecc1aad541c29db28923"`, - ); - await queryRunner.query( - `ALTER TABLE "roles" DROP CONSTRAINT "FK_0933e1dfb2993d672af1a98f08e"`, - ); - await queryRunner.query( - `ALTER TABLE "permissions" DROP CONSTRAINT "FK_36d7b8e1a331102ec9161e879ce"`, - ); - await queryRunner.query( - `ALTER TABLE "billing_plan" DROP CONSTRAINT "FK_e5f604154fb0c0cb99c9739fc69"`, - ); - await queryRunner.query( - `ALTER TABLE "payment" DROP CONSTRAINT "FK_6187e7a1d8072420d58d5340b09"`, - ); - await queryRunner.query( - `ALTER TABLE "payment" DROP CONSTRAINT "FK_be7fcc9fb8cd5a74cb602ec6c9b"`, - ); - await queryRunner.query( - `ALTER TABLE "comment" DROP CONSTRAINT "FK_5dec255234c5b7418f3d1e88ce4"`, - ); - await queryRunner.query( - `DROP INDEX "public"."IDX_f5665dcbec4177775b6edab2b9"`, - ); - await queryRunner.query( - `DROP INDEX "public"."IDX_200fdd3b43e7dfde885cf71bd3"`, - ); - await queryRunner.query(`DROP TABLE "blog_categories_category"`); - await queryRunner.query( - `DROP INDEX "public"."IDX_066934a149d9efba507443ce88"`, - ); - await queryRunner.query( - `DROP INDEX "public"."IDX_9572d27777384d535f77ed780d"`, - ); - await queryRunner.query(`DROP TABLE "blog_tags_tag"`); - await queryRunner.query( - `DROP INDEX "public"."IDX_8d7c566d5a234be0a646101326"`, - ); - await queryRunner.query( - `DROP INDEX "public"."IDX_7ad3d8541fbdb5a3d137c50fb4"`, - ); - await queryRunner.query(`DROP TABLE "user_organizations_organization"`); - await queryRunner.query(`DROP TABLE "testimonial"`); - await queryRunner.query(`DROP TABLE "tag"`); - await queryRunner.query(`DROP TABLE "blog"`); - await queryRunner.query(`DROP TABLE "like"`); - await queryRunner.query(`DROP TABLE "user"`); - await queryRunner.query(`DROP TABLE "faq"`); - await queryRunner.query(`DROP TABLE "contact"`); - await queryRunner.query(`DROP TABLE "invitation"`); - await queryRunner.query(`DROP TABLE "org_invite_token"`); - await queryRunner.query(`DROP TABLE "log"`); - await queryRunner.query(`DROP TABLE "email_queue"`); - await queryRunner.query(`DROP TABLE "job"`); - await queryRunner.query(`DROP TABLE "sms"`); - await queryRunner.query(`DROP TABLE "notification_setting"`); - await queryRunner.query(`DROP TABLE "help_center_topic"`); - await queryRunner.query(`DROP TABLE "organization"`); - await queryRunner.query(`DROP TABLE "user_organization"`); - await queryRunner.query(`DROP TABLE "product"`); - await queryRunner.query(`DROP TABLE "organization_member"`); - await queryRunner.query(`DROP TABLE "profile"`); - await queryRunner.query(`DROP TABLE "roles"`); - await queryRunner.query(`DROP TABLE "permissions"`); - await queryRunner.query(`DROP TABLE "billing_plan"`); - await queryRunner.query(`DROP TABLE "payment"`); - await queryRunner.query(`DROP TABLE "comment"`); - await queryRunner.query(`DROP TABLE "category"`); - } -} +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateUsersTable1723120636893 implements MigrationInterface { + name = "CreateUsersTable1723120636893"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "category" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_9c4e4a89e3674fc9f382d733f03" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "comment" ("id" SERIAL NOT NULL, "content" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "blogId" uuid, CONSTRAINT "PK_0b0e4bbc8415ec426f87f3a88e2" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "payment" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "billingPlanId" uuid, "amount" numeric(10,2) NOT NULL, "currency" character varying NOT NULL, "paymentServiceId" character varying, "status" "public"."payment_status_enum" NOT NULL, "provider" "public"."payment_provider_enum" NOT NULL, "organizationId" uuid, "description" character varying, "metadata" jsonb, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_fcaec7df5adf9cac408c686b2ab" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "billing_plan" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "organizationId" uuid NOT NULL, "name" character varying NOT NULL, "price" numeric(10,2) NOT NULL, "currency" character varying NOT NULL, "duration" character varying NOT NULL, "description" character varying, "features" text NOT NULL, CONSTRAINT "PK_63f4db8ca9063690ab4dfc3b3da" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "permissions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "category" "public"."permissions_category_enum" NOT NULL, "permission_list" boolean NOT NULL, "roleId" uuid, CONSTRAINT "PK_920331560282b8bd21bb02290df" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "roles" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(50) NOT NULL, "description" character varying(200), "organizationId" uuid, CONSTRAINT "PK_c1433d71a4838793a49dcad46ab" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "profile" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "first_name" character varying NOT NULL, "last_name" character varying NOT NULL, "phone_number" character varying, "avatarUrl" character varying NOT NULL, CONSTRAINT "PK_3dd8bfc97e4a77c70971591bdcb" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "organization_member" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userIdId" uuid, "organizationIdId" uuid, "roleId" uuid, "profileIdId" uuid, CONSTRAINT "PK_81dbbb093cbe0539c170f3d1484" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "product" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "description" character varying NOT NULL, "price" integer NOT NULL, "quantity" integer NOT NULL DEFAULT '1', "category" character varying NOT NULL, "image" character varying NOT NULL, "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "size" "public"."product_size_enum" NOT NULL DEFAULT 'Standard', "stock_status" "public"."product_stock_status_enum" NOT NULL DEFAULT 'out of stock', "orgId" uuid, "userId" uuid, CONSTRAINT "PK_bebc9158e480b949565b4dc7a82" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "user_organization" ("userId" uuid NOT NULL, "organizationId" uuid NOT NULL, "role" "public"."user_organization_role_enum" NOT NULL, CONSTRAINT "PK_6e6630567770ae6f0a76d05ce33" PRIMARY KEY ("userId", "organizationId"))`, + ); + await queryRunner.query( + `CREATE TABLE "organization" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "slug" character varying NOT NULL, "name" character varying NOT NULL, "email" character varying, "industry" character varying, "type" character varying, "country" character varying, "address" character varying, "state" character varying, "description" text, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "owner_id" uuid NOT NULL, CONSTRAINT "UQ_a08804baa7c5d5427067c49a31f" UNIQUE ("slug"), CONSTRAINT "PK_472c1f99a32def1b0abb219cd67" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "help_center_topic" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying NOT NULL, "content" character varying NOT NULL, "author" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_f1fd49531d0c8c8ecf09fca6e84" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "notification_setting" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying NOT NULL, "email_notifications" boolean NOT NULL, "push_notifications" boolean NOT NULL, "sms_notifications" boolean NOT NULL, CONSTRAINT "UQ_d210b9143572b7e8179c15f5f2a" UNIQUE ("user_id"), CONSTRAINT "PK_af85fd153b97ee9eacb505453fe" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "sms" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "phone_number" character varying NOT NULL, "message" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "senderId" uuid, CONSTRAINT "PK_60793c2f16aafe0513f8817eae8" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying NOT NULL, "user_id" character varying NOT NULL, "description" character varying NOT NULL, "location" character varying NOT NULL, "salary" character varying NOT NULL, "job_type" character varying NOT NULL, "company_name" character varying NOT NULL, CONSTRAINT "PK_98ab1c14ff8d1cf80d18703b92f" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "email_queue" ("id" SERIAL NOT NULL, "templateId" character varying NOT NULL, "recipient" character varying NOT NULL, "variables" json NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_b6c031a57087af131ed0176e17c" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "log" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying NOT NULL, "action" character varying NOT NULL, "details" character varying NOT NULL, "timestamp" TIMESTAMP NOT NULL, CONSTRAINT "PK_350604cbdf991d5930d9e618fbd" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "org_invite_token" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying NOT NULL, "expires_at" TIMESTAMP NOT NULL, "isActivated" boolean NOT NULL DEFAULT true, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "organizationId" uuid, CONSTRAINT "PK_7d3d1855ecf3e58dc28eb655afa" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "invitation" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying NOT NULL, "email" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "organizationId" uuid, "orgInviteTokenId" uuid, CONSTRAINT "PK_beb994737756c0f18a1c1f8669c" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "contact" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(100) NOT NULL, "email" character varying(100) NOT NULL, "phoneNumber" character varying(20) NOT NULL, "message" text NOT NULL, CONSTRAINT "PK_2cbbe00f59ab6b3bb5b8d19f989" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "faq" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "question" character varying NOT NULL, "answer" character varying NOT NULL, "category" character varying NOT NULL, "createdBy" character varying NOT NULL DEFAULT 'super_admin', CONSTRAINT "PK_d6f5a52b1a96dd8d0591f9fbc47" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "email" character varying NOT NULL, "password" character varying, "google_id" character varying, "isverified" boolean NOT NULL DEFAULT false, "role" "public"."user_role_enum" NOT NULL DEFAULT 'user', "otp" integer, "otp_expires_at" TIMESTAMP, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "is_deleted" boolean NOT NULL DEFAULT false, "deletedAt" TIMESTAMP, "passwordResetToken" character varying, "passwordResetExpires" bigint, "timezone" jsonb, "profileId" uuid, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "REL_9466682df91534dd95e4dbaa61" UNIQUE ("profileId"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "like" ("id" SERIAL NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "blogId" uuid, "userId" uuid, CONSTRAINT "PK_eff3e46d24d416b52a7e0ae4159" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "blog" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying NOT NULL, "content" character varying NOT NULL, "image_url" character varying, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "published_at" TIMESTAMP, "authorId" uuid, CONSTRAINT "PK_85c6532ad065a448e9de7638571" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "tag" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_8e4052373c579afc1471f526760" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "testimonial" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "user_id" character varying NOT NULL, "client_name" character varying NOT NULL, "client_position" character varying NOT NULL, "testimonial" character varying NOT NULL, CONSTRAINT "PK_e1aee1c726db2d336480c69f7cb" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "user_organizations_organization" ("userId" uuid NOT NULL, "organizationId" uuid NOT NULL, CONSTRAINT "PK_d89fbba617c90c71e2fc0bee26f" PRIMARY KEY ("userId", "organizationId"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_7ad3d8541fbdb5a3d137c50fb4" ON "user_organizations_organization" ("userId") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_8d7c566d5a234be0a646101326" ON "user_organizations_organization" ("organizationId") `, + ); + await queryRunner.query( + `CREATE TABLE "blog_tags_tag" ("blogId" uuid NOT NULL, "tagId" integer NOT NULL, CONSTRAINT "PK_163bef1f79bd1f15b07f75e072d" PRIMARY KEY ("blogId", "tagId"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_9572d27777384d535f77ed780d" ON "blog_tags_tag" ("blogId") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_066934a149d9efba507443ce88" ON "blog_tags_tag" ("tagId") `, + ); + await queryRunner.query( + `CREATE TABLE "blog_categories_category" ("blogId" uuid NOT NULL, "categoryId" integer NOT NULL, CONSTRAINT "PK_5f83120a485466f9e3fe7ada496" PRIMARY KEY ("blogId", "categoryId"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_200fdd3b43e7dfde885cf71bd3" ON "blog_categories_category" ("blogId") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_f5665dcbec4177775b6edab2b9" ON "blog_categories_category" ("categoryId") `, + ); + await queryRunner.query( + `ALTER TABLE "comment" ADD CONSTRAINT "FK_5dec255234c5b7418f3d1e88ce4" FOREIGN KEY ("blogId") REFERENCES "blog"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "payment" ADD CONSTRAINT "FK_be7fcc9fb8cd5a74cb602ec6c9b" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "payment" ADD CONSTRAINT "FK_6187e7a1d8072420d58d5340b09" FOREIGN KEY ("billingPlanId") REFERENCES "billing_plan"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "billing_plan" ADD CONSTRAINT "FK_e5f604154fb0c0cb99c9739fc69" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" ADD CONSTRAINT "FK_36d7b8e1a331102ec9161e879ce" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "roles" ADD CONSTRAINT "FK_0933e1dfb2993d672af1a98f08e" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "organization_member" ADD CONSTRAINT "FK_b7b4b3aecc1aad541c29db28923" FOREIGN KEY ("userIdId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "organization_member" ADD CONSTRAINT "FK_5ed6a30682214b2782545b389ee" FOREIGN KEY ("organizationIdId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "organization_member" ADD CONSTRAINT "FK_6f28dff88284c106b92c84d8625" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "organization_member" ADD CONSTRAINT "FK_72d91423c27a90fd9f56925b30a" FOREIGN KEY ("profileIdId") REFERENCES "profile"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "product" ADD CONSTRAINT "FK_4001796e6dec57fa1424e6ffe22" FOREIGN KEY ("orgId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "product" ADD CONSTRAINT "FK_329b8ae12068b23da547d3b4798" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "user_organization" ADD CONSTRAINT "FK_29c3c8cc3ea9db22e4a347f4b5a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "user_organization" ADD CONSTRAINT "FK_7143f31467178a6164a42426c15" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "sms" ADD CONSTRAINT "FK_5e4a3ebde193729147d95e0822c" FOREIGN KEY ("senderId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "org_invite_token" ADD CONSTRAINT "FK_4775270ba8c3a34c05ce4ec9951" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "invitation" ADD CONSTRAINT "FK_5c00d7d515395f91bd1fee19f32" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "invitation" ADD CONSTRAINT "FK_043292be3660aa8e5a46de7c4d7" FOREIGN KEY ("orgInviteTokenId") REFERENCES "org_invite_token"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD CONSTRAINT "FK_9466682df91534dd95e4dbaa616" FOREIGN KEY ("profileId") REFERENCES "profile"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "like" ADD CONSTRAINT "FK_1b343f6df7583577dffcd777120" FOREIGN KEY ("blogId") REFERENCES "blog"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "like" ADD CONSTRAINT "FK_e8fb739f08d47955a39850fac23" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "blog" ADD CONSTRAINT "FK_a001483d5ba65dad16557cd6ddb" FOREIGN KEY ("authorId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "user_organizations_organization" ADD CONSTRAINT "FK_7ad3d8541fbdb5a3d137c50fb40" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "user_organizations_organization" ADD CONSTRAINT "FK_8d7c566d5a234be0a6461013269" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "blog_tags_tag" ADD CONSTRAINT "FK_9572d27777384d535f77ed780d0" FOREIGN KEY ("blogId") REFERENCES "blog"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "blog_tags_tag" ADD CONSTRAINT "FK_066934a149d9efba507443ce889" FOREIGN KEY ("tagId") REFERENCES "tag"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "blog_categories_category" ADD CONSTRAINT "FK_200fdd3b43e7dfde885cf71bd3a" FOREIGN KEY ("blogId") REFERENCES "blog"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "blog_categories_category" ADD CONSTRAINT "FK_f5665dcbec4177775b6edab2b9b" FOREIGN KEY ("categoryId") REFERENCES "category"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "blog_categories_category" DROP CONSTRAINT "FK_f5665dcbec4177775b6edab2b9b"`, + ); + await queryRunner.query( + `ALTER TABLE "blog_categories_category" DROP CONSTRAINT "FK_200fdd3b43e7dfde885cf71bd3a"`, + ); + await queryRunner.query( + `ALTER TABLE "blog_tags_tag" DROP CONSTRAINT "FK_066934a149d9efba507443ce889"`, + ); + await queryRunner.query( + `ALTER TABLE "blog_tags_tag" DROP CONSTRAINT "FK_9572d27777384d535f77ed780d0"`, + ); + await queryRunner.query( + `ALTER TABLE "user_organizations_organization" DROP CONSTRAINT "FK_8d7c566d5a234be0a6461013269"`, + ); + await queryRunner.query( + `ALTER TABLE "user_organizations_organization" DROP CONSTRAINT "FK_7ad3d8541fbdb5a3d137c50fb40"`, + ); + await queryRunner.query( + `ALTER TABLE "blog" DROP CONSTRAINT "FK_a001483d5ba65dad16557cd6ddb"`, + ); + await queryRunner.query( + `ALTER TABLE "like" DROP CONSTRAINT "FK_e8fb739f08d47955a39850fac23"`, + ); + await queryRunner.query( + `ALTER TABLE "like" DROP CONSTRAINT "FK_1b343f6df7583577dffcd777120"`, + ); + await queryRunner.query( + `ALTER TABLE "user" DROP CONSTRAINT "FK_9466682df91534dd95e4dbaa616"`, + ); + await queryRunner.query( + `ALTER TABLE "invitation" DROP CONSTRAINT "FK_043292be3660aa8e5a46de7c4d7"`, + ); + await queryRunner.query( + `ALTER TABLE "invitation" DROP CONSTRAINT "FK_5c00d7d515395f91bd1fee19f32"`, + ); + await queryRunner.query( + `ALTER TABLE "org_invite_token" DROP CONSTRAINT "FK_4775270ba8c3a34c05ce4ec9951"`, + ); + await queryRunner.query( + `ALTER TABLE "sms" DROP CONSTRAINT "FK_5e4a3ebde193729147d95e0822c"`, + ); + await queryRunner.query( + `ALTER TABLE "user_organization" DROP CONSTRAINT "FK_7143f31467178a6164a42426c15"`, + ); + await queryRunner.query( + `ALTER TABLE "user_organization" DROP CONSTRAINT "FK_29c3c8cc3ea9db22e4a347f4b5a"`, + ); + await queryRunner.query( + `ALTER TABLE "product" DROP CONSTRAINT "FK_329b8ae12068b23da547d3b4798"`, + ); + await queryRunner.query( + `ALTER TABLE "product" DROP CONSTRAINT "FK_4001796e6dec57fa1424e6ffe22"`, + ); + await queryRunner.query( + `ALTER TABLE "organization_member" DROP CONSTRAINT "FK_72d91423c27a90fd9f56925b30a"`, + ); + await queryRunner.query( + `ALTER TABLE "organization_member" DROP CONSTRAINT "FK_6f28dff88284c106b92c84d8625"`, + ); + await queryRunner.query( + `ALTER TABLE "organization_member" DROP CONSTRAINT "FK_5ed6a30682214b2782545b389ee"`, + ); + await queryRunner.query( + `ALTER TABLE "organization_member" DROP CONSTRAINT "FK_b7b4b3aecc1aad541c29db28923"`, + ); + await queryRunner.query( + `ALTER TABLE "roles" DROP CONSTRAINT "FK_0933e1dfb2993d672af1a98f08e"`, + ); + await queryRunner.query( + `ALTER TABLE "permissions" DROP CONSTRAINT "FK_36d7b8e1a331102ec9161e879ce"`, + ); + await queryRunner.query( + `ALTER TABLE "billing_plan" DROP CONSTRAINT "FK_e5f604154fb0c0cb99c9739fc69"`, + ); + await queryRunner.query( + `ALTER TABLE "payment" DROP CONSTRAINT "FK_6187e7a1d8072420d58d5340b09"`, + ); + await queryRunner.query( + `ALTER TABLE "payment" DROP CONSTRAINT "FK_be7fcc9fb8cd5a74cb602ec6c9b"`, + ); + await queryRunner.query( + `ALTER TABLE "comment" DROP CONSTRAINT "FK_5dec255234c5b7418f3d1e88ce4"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_f5665dcbec4177775b6edab2b9"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_200fdd3b43e7dfde885cf71bd3"`, + ); + await queryRunner.query(`DROP TABLE "blog_categories_category"`); + await queryRunner.query( + `DROP INDEX "public"."IDX_066934a149d9efba507443ce88"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_9572d27777384d535f77ed780d"`, + ); + await queryRunner.query(`DROP TABLE "blog_tags_tag"`); + await queryRunner.query( + `DROP INDEX "public"."IDX_8d7c566d5a234be0a646101326"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_7ad3d8541fbdb5a3d137c50fb4"`, + ); + await queryRunner.query(`DROP TABLE "user_organizations_organization"`); + await queryRunner.query(`DROP TABLE "testimonial"`); + await queryRunner.query(`DROP TABLE "tag"`); + await queryRunner.query(`DROP TABLE "blog"`); + await queryRunner.query(`DROP TABLE "like"`); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`DROP TABLE "faq"`); + await queryRunner.query(`DROP TABLE "contact"`); + await queryRunner.query(`DROP TABLE "invitation"`); + await queryRunner.query(`DROP TABLE "org_invite_token"`); + await queryRunner.query(`DROP TABLE "log"`); + await queryRunner.query(`DROP TABLE "email_queue"`); + await queryRunner.query(`DROP TABLE "job"`); + await queryRunner.query(`DROP TABLE "sms"`); + await queryRunner.query(`DROP TABLE "notification_setting"`); + await queryRunner.query(`DROP TABLE "help_center_topic"`); + await queryRunner.query(`DROP TABLE "organization"`); + await queryRunner.query(`DROP TABLE "user_organization"`); + await queryRunner.query(`DROP TABLE "product"`); + await queryRunner.query(`DROP TABLE "organization_member"`); + await queryRunner.query(`DROP TABLE "profile"`); + await queryRunner.query(`DROP TABLE "roles"`); + await queryRunner.query(`DROP TABLE "permissions"`); + await queryRunner.query(`DROP TABLE "billing_plan"`); + await queryRunner.query(`DROP TABLE "payment"`); + await queryRunner.query(`DROP TABLE "comment"`); + await queryRunner.query(`DROP TABLE "category"`); + } +} diff --git a/src/models/Testimonial.ts b/src/models/Testimonial.ts index 0bebc18a..6fd3dfe9 100644 --- a/src/models/Testimonial.ts +++ b/src/models/Testimonial.ts @@ -1,20 +1,20 @@ -import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; -import ExtendedBaseEntity from "./extended-base-entity"; - -@Entity() -export class Testimonial extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - user_id: string; - - @Column() - client_name: string; - - @Column() - client_position: string; - - @Column() - testimonial: string; -} +import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; +import ExtendedBaseEntity from "./extended-base-entity"; + +@Entity() +export class Testimonial extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + user_id: string; + + @Column() + client_name: string; + + @Column() + client_position: string; + + @Column() + testimonial: string; +} diff --git a/src/models/blog.ts b/src/models/blog.ts index 14d8c987..dcfb8642 100644 --- a/src/models/blog.ts +++ b/src/models/blog.ts @@ -1,57 +1,57 @@ -import { - Column, - CreateDateColumn, - Entity, - JoinTable, - ManyToMany, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from "typeorm"; -import { Category } from "./category"; -import { Comment } from "./comment"; -import { Like } from "./like"; -import { Tag } from "./tag"; -import { User } from "./user"; - -@Entity() -export class Blog { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - title: string; - - @Column() - content: string; - - @Column({ nullable: true }) - image_url: string; - - @ManyToOne(() => User, (user) => user.blogs) - author: User; - - @OneToMany(() => Comment, (comment) => comment.blog) - comments: Comment[]; - - @ManyToMany(() => Tag, (tag) => tag.blogs) - @JoinTable() - tags: Tag[]; - - @ManyToMany(() => Category, (category) => category.blogs) - @JoinTable() - categories: Category[]; - - @OneToMany(() => Like, (like) => like.blog) - likes: Like[]; - - @CreateDateColumn() - created_at: Date; - - @UpdateDateColumn() - updated_at: Date; - - @Column({ nullable: true }) - published_at: Date; -} +import { + Column, + CreateDateColumn, + Entity, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from "typeorm"; +import { Category } from "./category"; +import { Comment } from "./comment"; +import { Like } from "./like"; +import { Tag } from "./tag"; +import { User } from "./user"; + +@Entity() +export class Blog { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + title: string; + + @Column() + content: string; + + @Column({ nullable: true }) + image_url: string; + + @ManyToOne(() => User, (user) => user.blogs) + author: User; + + @OneToMany(() => Comment, (comment) => comment.blog) + comments: Comment[]; + + @ManyToMany(() => Tag, (tag) => tag.blogs) + @JoinTable() + tags: Tag[]; + + @ManyToMany(() => Category, (category) => category.blogs) + @JoinTable() + categories: Category[]; + + @OneToMany(() => Like, (like) => like.blog) + likes: Like[]; + + @CreateDateColumn() + created_at: Date; + + @UpdateDateColumn() + updated_at: Date; + + @Column({ nullable: true }) + published_at: Date; +} diff --git a/src/models/comment.ts b/src/models/comment.ts index d80d298a..b492e1ae 100644 --- a/src/models/comment.ts +++ b/src/models/comment.ts @@ -1,34 +1,34 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - CreateDateColumn, - UpdateDateColumn, - JoinColumn, -} from "typeorm"; -import { Blog } from "./blog"; -import { User } from "./user"; - -@Entity() -export class Comment { - @PrimaryGeneratedColumn() - id: number; - - @Column() - content: string; - - @ManyToOne(() => Blog, (blog) => blog.comments) - @JoinColumn({ name: "blog_id" }) - blog: Blog; - - @ManyToOne(() => User, (user) => user.comments, { eager: true }) - @JoinColumn({ name: "user_id" }) - author: User; - - @CreateDateColumn() - created_at: Date; - - @UpdateDateColumn() - updated_at: Date; -} +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, + JoinColumn, +} from "typeorm"; +import { Blog } from "./blog"; +import { User } from "./user"; + +@Entity() +export class Comment { + @PrimaryGeneratedColumn() + id: number; + + @Column() + content: string; + + @ManyToOne(() => Blog, (blog) => blog.comments) + @JoinColumn({ name: "blog_id" }) + blog: Blog; + + @ManyToOne(() => User, (user) => user.comments, { eager: true }) + @JoinColumn({ name: "user_id" }) + author: User; + + @CreateDateColumn() + created_at: Date; + + @UpdateDateColumn() + updated_at: Date; +} diff --git a/src/models/contact.ts b/src/models/contact.ts index 918a155f..e0b02dd8 100644 --- a/src/models/contact.ts +++ b/src/models/contact.ts @@ -1,16 +1,16 @@ -import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; - -@Entity() -export class Contact { - @PrimaryGeneratedColumn("uuid") - id!: string; - - @Column({ type: "varchar", length: 100 }) - name!: string; - - @Column({ type: "varchar", length: 100 }) - email!: string; - - @Column({ type: "text" }) - message!: string; -} +import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; + +@Entity() +export class Contact { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ type: "varchar", length: 100 }) + name!: string; + + @Column({ type: "varchar", length: 100 }) + email!: string; + + @Column({ type: "text" }) + message!: string; +} diff --git a/src/models/emailQueue.ts b/src/models/emailQueue.ts index 65396fba..2f868333 100644 --- a/src/models/emailQueue.ts +++ b/src/models/emailQueue.ts @@ -1,27 +1,27 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, -} from "typeorm"; - -@Entity() -export class EmailQueue { - @PrimaryGeneratedColumn() - id: number; - - @Column() - templateId: string; - - @Column() - recipient: string; - - @Column("json") - variables: Record; - - @CreateDateColumn() - createdAt: Date; - - // @Column({ default: false }) - // isActive: boolean; -} +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from "typeorm"; + +@Entity() +export class EmailQueue { + @PrimaryGeneratedColumn() + id: number; + + @Column() + templateId: string; + + @Column() + recipient: string; + + @Column("json") + variables: Record; + + @CreateDateColumn() + createdAt: Date; + + // @Column({ default: false }) + // isActive: boolean; +} diff --git a/src/models/extended-base-entity.ts b/src/models/extended-base-entity.ts index 418ffe21..76b16ece 100644 --- a/src/models/extended-base-entity.ts +++ b/src/models/extended-base-entity.ts @@ -1,16 +1,16 @@ -import { BaseEntity, BeforeInsert, BeforeUpdate } from 'typeorm'; -import { validateOrReject } from 'class-validator'; - -class ExtendedBaseEntity extends BaseEntity { - @BeforeInsert() - async validateOnInsert() { - await validateOrReject(this); - } - - @BeforeUpdate() - async validateOnUpdate() { - await validateOrReject(this, { skipMissingProperties: true }); - } -} - -export default ExtendedBaseEntity; +import { BaseEntity, BeforeInsert, BeforeUpdate } from 'typeorm'; +import { validateOrReject } from 'class-validator'; + +class ExtendedBaseEntity extends BaseEntity { + @BeforeInsert() + async validateOnInsert() { + await validateOrReject(this); + } + + @BeforeUpdate() + async validateOnUpdate() { + await validateOrReject(this, { skipMissingProperties: true }); + } +} + +export default ExtendedBaseEntity; diff --git a/src/models/faq.ts b/src/models/faq.ts index 86932bec..f1e371fa 100644 --- a/src/models/faq.ts +++ b/src/models/faq.ts @@ -1,23 +1,23 @@ -import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; -import ExtendedBaseEntity from "./extended-base-entity"; -import { UserRole } from "../enums/userRoles"; - -@Entity() -class FAQ extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column({ nullable: false }) - question: string; - - @Column({ nullable: false }) - answer: string; - - @Column({ nullable: false }) - category: string; - - @Column({ nullable: false, default: UserRole.SUPER_ADMIN }) - createdBy: string; -} - -export { FAQ }; +import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { UserRole } from "../enums/userRoles"; + +@Entity() +class FAQ extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ nullable: false }) + question: string; + + @Column({ nullable: false }) + answer: string; + + @Column({ nullable: false }) + category: string; + + @Column({ nullable: false, default: UserRole.SUPER_ADMIN }) + createdBy: string; +} + +export { FAQ }; diff --git a/src/models/helpcentertopic.ts b/src/models/helpcentertopic.ts index 6f62e0cc..d70f9979 100644 --- a/src/models/helpcentertopic.ts +++ b/src/models/helpcentertopic.ts @@ -1,29 +1,29 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, -} from "typeorm"; -import ExtendedBaseEntity from "./extended-base-entity"; - -@Entity() -export class HelpCenterTopic extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - title: string; - - @Column() - content: string; - - @Column() - author: string; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from "typeorm"; +import ExtendedBaseEntity from "./extended-base-entity"; + +@Entity() +export class HelpCenterTopic extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + title: string; + + @Column() + content: string; + + @Column() + author: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/src/models/invitation.ts b/src/models/invitation.ts index e54e9800..6e170996 100644 --- a/src/models/invitation.ts +++ b/src/models/invitation.ts @@ -1,36 +1,36 @@ -import { - Entity, - Column, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from "typeorm"; - -import { Organization } from "."; - -@Entity() -export class Invitation { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column({ type: "uuid", nullable: false }) - token: string; - - @Column({ nullable: true }) - email: string; - - @Column({ default: false }) - isGeneric: boolean; - - @Column({ default: false }) - isAccepted: boolean; - - @ManyToOne(() => Organization) - organization: Organization; - - @UpdateDateColumn() - created_at: Date; - - @UpdateDateColumn() - updated_at: Date; -} +import { + Entity, + Column, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from "typeorm"; + +import { Organization } from "."; + +@Entity() +export class Invitation { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ type: "uuid", nullable: false }) + token: string; + + @Column({ nullable: true }) + email: string; + + @Column({ default: false }) + isGeneric: boolean; + + @Column({ default: false }) + isAccepted: boolean; + + @ManyToOne(() => Organization) + organization: Organization; + + @UpdateDateColumn() + created_at: Date; + + @UpdateDateColumn() + updated_at: Date; +} diff --git a/src/models/job.ts b/src/models/job.ts index 450ed281..fb566038 100644 --- a/src/models/job.ts +++ b/src/models/job.ts @@ -1,29 +1,29 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm"; -import ExtendedBaseEntity from "./extended-base-entity"; - -@Entity() -export class Job extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - title: string; - - @Column() - user_id: string; - - @Column() - description: string; - - @Column() - location: string; - - @Column() - salary: string; - - @Column() - job_type: string; - - @Column() - company_name: string; -} +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm"; +import ExtendedBaseEntity from "./extended-base-entity"; + +@Entity() +export class Job extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + title: string; + + @Column() + user_id: string; + + @Column() + description: string; + + @Column() + location: string; + + @Column() + salary: string; + + @Column() + job_type: string; + + @Column() + company_name: string; +} diff --git a/src/models/like.ts b/src/models/like.ts index ef8178e3..435215c1 100644 --- a/src/models/like.ts +++ b/src/models/like.ts @@ -1,23 +1,23 @@ -import { - Entity, - PrimaryGeneratedColumn, - ManyToOne, - CreateDateColumn, -} from "typeorm"; -import { Blog } from "./blog"; -import { User } from "./user"; - -@Entity() -export class Like { - @PrimaryGeneratedColumn() - id: number; - - @ManyToOne(() => Blog, (blog) => blog.likes) - blog: Blog; - - @ManyToOne(() => User, (user) => user.likes) - user: User; - - @CreateDateColumn() - created_at: Date; -} +import { + Entity, + PrimaryGeneratedColumn, + ManyToOne, + CreateDateColumn, +} from "typeorm"; +import { Blog } from "./blog"; +import { User } from "./user"; + +@Entity() +export class Like { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => Blog, (blog) => blog.likes) + blog: Blog; + + @ManyToOne(() => User, (user) => user.likes) + user: User; + + @CreateDateColumn() + created_at: Date; +} diff --git a/src/models/log.ts b/src/models/log.ts index f68d4470..cc52e0bc 100644 --- a/src/models/log.ts +++ b/src/models/log.ts @@ -1,19 +1,19 @@ -import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; - -@Entity() -export class Log { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - user_id: string; - - @Column() - action: string; - - @Column() - details: string; - - @Column() - timestamp: Date; -} +import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; + +@Entity() +export class Log { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + user_id: string; + + @Column() + action: string; + + @Column() + details: string; + + @Column() + timestamp: Date; +} diff --git a/src/models/newsLetterSubscription.ts b/src/models/newsLetterSubscription.ts index a9a41829..8b72a259 100644 --- a/src/models/newsLetterSubscription.ts +++ b/src/models/newsLetterSubscription.ts @@ -1,18 +1,18 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, -} from "typeorm"; - -@Entity() -export class NewsLetterSubscriber { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - email: string; - - @Column() - isSubscribe: boolean; -} +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from "typeorm"; + +@Entity() +export class NewsLetterSubscriber { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + email: string; + + @Column() + isSubscribe: boolean; +} diff --git a/src/models/notification.ts b/src/models/notification.ts index b56c1633..097e01e5 100644 --- a/src/models/notification.ts +++ b/src/models/notification.ts @@ -1,32 +1,32 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - CreateDateColumn, - UpdateDateColumn, -} from "typeorm"; -import { User } from "./user"; - -@Entity() -class Notification { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - message: string; - - @Column({ default: false }) - isRead: boolean; - - @ManyToOne(() => User, (user) => user.notifications, { onDelete: "CASCADE" }) - user: User; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} - -export { Notification }; +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, +} from "typeorm"; +import { User } from "./user"; + +@Entity() +class Notification { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + message: string; + + @Column({ default: false }) + isRead: boolean; + + @ManyToOne(() => User, (user) => user.notifications, { onDelete: "CASCADE" }) + user: User; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} + +export { Notification }; diff --git a/src/models/notificationsettings.ts b/src/models/notificationsettings.ts index f1a2bf14..5bc2edbe 100644 --- a/src/models/notificationsettings.ts +++ b/src/models/notificationsettings.ts @@ -1,26 +1,26 @@ -import { Entity, PrimaryGeneratedColumn, Column, Unique } from "typeorm"; -import { IsBoolean, IsUUID } from "class-validator"; -import ExtendedBaseEntity from "./extended-base-entity"; - -@Entity() -@Unique(["user_id"]) -export class NotificationSetting extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - @IsUUID() - user_id: string; - - @Column() - @IsBoolean() - email_notifications: boolean; - - @Column() - @IsBoolean() - push_notifications: boolean; - - @Column() - @IsBoolean() - sms_notifications: boolean; -} +import { Entity, PrimaryGeneratedColumn, Column, Unique } from "typeorm"; +import { IsBoolean, IsUUID } from "class-validator"; +import ExtendedBaseEntity from "./extended-base-entity"; + +@Entity() +@Unique(["user_id"]) +export class NotificationSetting extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + @IsUUID() + user_id: string; + + @Column() + @IsBoolean() + email_notifications: boolean; + + @Column() + @IsBoolean() + push_notifications: boolean; + + @Column() + @IsBoolean() + sms_notifications: boolean; +} diff --git a/src/models/organization-member.ts b/src/models/organization-member.ts index 0a71ac07..9a33ff8e 100644 --- a/src/models/organization-member.ts +++ b/src/models/organization-member.ts @@ -1,27 +1,27 @@ -import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; -import ExtendedBaseEntity from "./extended-base-entity"; -import { Organization } from "./organization"; -import { OrganizationRole } from "./organization-role.entity"; -import { Profile } from "./profile"; -import { User } from "./user"; - -@Entity() -export class OrganizationMember extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @ManyToOne(() => User, (user) => user.organizationMembers) - user_id: User; - - @ManyToOne( - () => Organization, - (organization) => organization.organizationMembers, - ) - organization_id: Organization; - - @ManyToOne(() => OrganizationRole, (role) => role.organizationMembers) - role: OrganizationRole; - - @ManyToOne(() => Profile) - profile_id: Profile; -} +import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { Organization } from "./organization"; +import { OrganizationRole } from "./organization-role.entity"; +import { Profile } from "./profile"; +import { User } from "./user"; + +@Entity() +export class OrganizationMember extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @ManyToOne(() => User, (user) => user.organizationMembers) + user_id: User; + + @ManyToOne( + () => Organization, + (organization) => organization.organizationMembers, + ) + organization_id: Organization; + + @ManyToOne(() => OrganizationRole, (role) => role.organizationMembers) + role: OrganizationRole; + + @ManyToOne(() => Profile) + profile_id: Profile; +} diff --git a/src/models/organization-role.entity.ts b/src/models/organization-role.entity.ts index 0ca7ab3a..374d3da6 100644 --- a/src/models/organization-role.entity.ts +++ b/src/models/organization-role.entity.ts @@ -1,36 +1,36 @@ -import { - Column, - Entity, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, -} from "typeorm"; -import ExtendedBaseEntity from "./extended-base-entity"; -import { Organization } from "./organization"; -import { OrganizationMember } from "./organization-member"; -import { Permissions } from "./permissions.entity"; - -@Entity("roles") -export class OrganizationRole extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column({ length: 50, nullable: false }) - name: string; - - @Column({ length: 200, nullable: true }) - description: string; - - @OneToMany(() => Permissions, (permission) => permission.role, { - eager: false, - }) - permissions: Permissions[]; - - @ManyToOne(() => Organization, (organization) => organization.role, { - eager: false, - }) - organization: Organization; - - @OneToMany(() => OrganizationMember, (member) => member.role) - organizationMembers: OrganizationMember[]; -} +import { + Column, + Entity, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from "typeorm"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { Organization } from "./organization"; +import { OrganizationMember } from "./organization-member"; +import { Permissions } from "./permissions.entity"; + +@Entity("roles") +export class OrganizationRole extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ length: 50, nullable: false }) + name: string; + + @Column({ length: 200, nullable: true }) + description: string; + + @OneToMany(() => Permissions, (permission) => permission.role, { + eager: false, + }) + permissions: Permissions[]; + + @ManyToOne(() => Organization, (organization) => organization.role, { + eager: false, + }) + organization: Organization; + + @OneToMany(() => OrganizationMember, (member) => member.role) + organizationMembers: OrganizationMember[]; +} diff --git a/src/models/organization.ts b/src/models/organization.ts index 8847ddc7..039fbe7a 100644 --- a/src/models/organization.ts +++ b/src/models/organization.ts @@ -1,94 +1,94 @@ -import { - BeforeInsert, - Column, - Entity, - ManyToMany, - OneToMany, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from "typeorm"; -import { v4 as uuidv4 } from "uuid"; -import { User } from "."; -import { BillingPlan } from "./billing-plan"; -import ExtendedBaseEntity from "./extended-base-entity"; -import { OrganizationMember } from "./organization-member"; -import { OrganizationRole } from "./organization-role.entity"; -import { Payment } from "./payment"; -import { Product } from "./product"; -import { UserOrganization } from "./user-organisation"; - -@Entity() -export class Organization extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column({ unique: true }) - slug: string; - - @Column() - name: string; - - @Column({ nullable: true }) - email: string; - - @Column({ nullable: true }) - industry: string; - - @Column({ nullable: true }) - type: string; - - @Column({ nullable: true }) - country: string; - - @Column({ nullable: true }) - address: string; - - @Column({ nullable: true }) - state: string; - - @Column("text", { nullable: true }) - description: string; - - @UpdateDateColumn() - created_at: Date; - - @UpdateDateColumn() - updated_at: Date; - - @Column("uuid") - owner_id: string; - - @OneToMany( - () => UserOrganization, - (userOrganization) => userOrganization.organization, - ) - userOrganizations: UserOrganization[]; - - @ManyToMany(() => User, (user) => user.organizations) - users: User[]; - - @OneToMany(() => Payment, (payment) => payment.organization) - payments: Payment[]; - - @OneToMany(() => BillingPlan, (billingPlan) => billingPlan.organization) - billingPlans: BillingPlan[]; - - @OneToMany(() => Product, (product) => product.org, { cascade: true }) - products: Product[]; - - @OneToMany(() => OrganizationRole, (role) => role.organization, { - eager: false, - }) - role: OrganizationRole; - - @OneToMany( - () => OrganizationMember, - (organizationMember) => organizationMember.organization_id, - ) - organizationMembers: OrganizationMember[]; - - @BeforeInsert() - generateSlug() { - this.slug = uuidv4(); - } -} +import { + BeforeInsert, + Column, + Entity, + ManyToMany, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from "typeorm"; +import { v4 as uuidv4 } from "uuid"; +import { User } from "."; +import { BillingPlan } from "./billing-plan"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { OrganizationMember } from "./organization-member"; +import { OrganizationRole } from "./organization-role.entity"; +import { Payment } from "./payment"; +import { Product } from "./product"; +import { UserOrganization } from "./user-organisation"; + +@Entity() +export class Organization extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ unique: true }) + slug: string; + + @Column() + name: string; + + @Column({ nullable: true }) + email: string; + + @Column({ nullable: true }) + industry: string; + + @Column({ nullable: true }) + type: string; + + @Column({ nullable: true }) + country: string; + + @Column({ nullable: true }) + address: string; + + @Column({ nullable: true }) + state: string; + + @Column("text", { nullable: true }) + description: string; + + @UpdateDateColumn() + created_at: Date; + + @UpdateDateColumn() + updated_at: Date; + + @Column("uuid") + owner_id: string; + + @OneToMany( + () => UserOrganization, + (userOrganization) => userOrganization.organization, + ) + userOrganizations: UserOrganization[]; + + @ManyToMany(() => User, (user) => user.organizations) + users: User[]; + + @OneToMany(() => Payment, (payment) => payment.organization) + payments: Payment[]; + + @OneToMany(() => BillingPlan, (billingPlan) => billingPlan.organization) + billingPlans: BillingPlan[]; + + @OneToMany(() => Product, (product) => product.org, { cascade: true }) + products: Product[]; + + @OneToMany(() => OrganizationRole, (role) => role.organization, { + eager: false, + }) + role: OrganizationRole; + + @OneToMany( + () => OrganizationMember, + (organizationMember) => organizationMember.organization_id, + ) + organizationMembers: OrganizationMember[]; + + @BeforeInsert() + generateSlug() { + this.slug = uuidv4(); + } +} diff --git a/src/models/payment.ts b/src/models/payment.ts index 43300021..2db7d173 100644 --- a/src/models/payment.ts +++ b/src/models/payment.ts @@ -1,65 +1,65 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, -} from "typeorm"; -import { Organization } from "./organization"; -import { BillingPlan } from "./billing-plan"; - -@Entity() -export class Payment { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column("uuid", { nullable: true }) - billingPlanId: string; - - @Column("decimal", { precision: 10, scale: 2 }) - amount: number; - - @Column() - currency: string; - - @Column({ nullable: true }) - paymentServiceId: string | null; - - @Column({ - type: "enum", - enum: ["pending", "completed", "failed"], - }) - status: "pending" | "completed" | "failed"; - - @Column({ - type: "enum", - enum: ["stripe", "flutterwave", "lemonsqueezy", "paystack"], - }) - provider: "stripe" | "flutterwave" | "lemonsqueezy" | "paystack"; - - @Column("uuid", { nullable: true }) - organizationId: string | null; - - @ManyToOne(() => Organization, (organization) => organization.payments, { - nullable: true, - }) - organization: Organization | null; - - @ManyToOne(() => BillingPlan, (billingPlan) => billingPlan.payments, { - onDelete: "CASCADE", - }) - billingPlan: BillingPlan; - - @Column({ nullable: true }) - description: string; - - @Column("jsonb", { nullable: true }) - metadata: object; - - @CreateDateColumn() - created_at: Date; - - @UpdateDateColumn() - updated_at: Date; -} +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, +} from "typeorm"; +import { Organization } from "./organization"; +import { BillingPlan } from "./billing-plan"; + +@Entity() +export class Payment { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column("uuid", { nullable: true }) + billingPlanId: string; + + @Column("decimal", { precision: 10, scale: 2 }) + amount: number; + + @Column() + currency: string; + + @Column({ nullable: true }) + paymentServiceId: string | null; + + @Column({ + type: "enum", + enum: ["pending", "completed", "failed"], + }) + status: "pending" | "completed" | "failed"; + + @Column({ + type: "enum", + enum: ["stripe", "flutterwave", "lemonsqueezy", "paystack"], + }) + provider: "stripe" | "flutterwave" | "lemonsqueezy" | "paystack"; + + @Column("uuid", { nullable: true }) + organizationId: string | null; + + @ManyToOne(() => Organization, (organization) => organization.payments, { + nullable: true, + }) + organization: Organization | null; + + @ManyToOne(() => BillingPlan, (billingPlan) => billingPlan.payments, { + onDelete: "CASCADE", + }) + billingPlan: BillingPlan; + + @Column({ nullable: true }) + description: string; + + @Column("jsonb", { nullable: true }) + metadata: object; + + @CreateDateColumn() + created_at: Date; + + @UpdateDateColumn() + updated_at: Date; +} diff --git a/src/models/permissions.entity.ts b/src/models/permissions.entity.ts index 23118fed..5cbe7481 100644 --- a/src/models/permissions.entity.ts +++ b/src/models/permissions.entity.ts @@ -1,23 +1,23 @@ -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; -import { PermissionCategory } from "../enums/permission-category.enum"; -import ExtendedBaseEntity from "./extended-base-entity"; -import { OrganizationRole } from "./organization-role.entity"; - -@Entity() -export class Permissions extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - @Column({ - type: "enum", - enum: PermissionCategory, - }) - category: PermissionCategory; - - @Column({ type: "boolean", nullable: false }) - permission_list: boolean; - - @ManyToOne(() => OrganizationRole, (role) => role.permissions, { - eager: false, - }) - role: OrganizationRole; -} +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { PermissionCategory } from "../enums/permission-category.enum"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { OrganizationRole } from "./organization-role.entity"; + +@Entity() +export class Permissions extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + @Column({ + type: "enum", + enum: PermissionCategory, + }) + category: PermissionCategory; + + @Column({ type: "boolean", nullable: false }) + permission_list: boolean; + + @ManyToOne(() => OrganizationRole, (role) => role.permissions, { + eager: false, + }) + role: OrganizationRole; +} diff --git a/src/models/product.ts b/src/models/product.ts index aadd5761..a7a4fe5c 100644 --- a/src/models/product.ts +++ b/src/models/product.ts @@ -1,61 +1,61 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - UpdateDateColumn, - CreateDateColumn, -} from "typeorm"; -import { Organization } from "./organization"; -import ExtendedBaseEntity from "./extended-base-entity"; -import { ProductSize, StockStatus } from "../enums/product"; -import { User } from "./user"; -@Entity() -export class Product extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - name: string; - - @Column() - description: string; - - @Column() - price: number; - - @Column({ default: 1 }) - quantity: number; - - @Column() - category: string; - - @Column() - image: string; - - @UpdateDateColumn() - updated_at: Date; - - @CreateDateColumn() - created_at: Date; - - @Column({ - type: "enum", - enum: ProductSize, - default: ProductSize.STANDARD, - }) - size: ProductSize; - - @Column({ - type: "enum", - enum: StockStatus, - default: StockStatus.OUT_STOCK, - }) - stock_status: StockStatus; - - @ManyToOne(() => Organization, (org) => org.products) - org: Organization; - - @ManyToOne(() => User, (user) => user.products) - user: User; -} +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + UpdateDateColumn, + CreateDateColumn, +} from "typeorm"; +import { Organization } from "./organization"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { ProductSize, StockStatus } from "../enums/product"; +import { User } from "./user"; +@Entity() +export class Product extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + name: string; + + @Column() + description: string; + + @Column() + price: number; + + @Column({ default: 1 }) + quantity: number; + + @Column() + category: string; + + @Column() + image: string; + + @UpdateDateColumn() + updated_at: Date; + + @CreateDateColumn() + created_at: Date; + + @Column({ + type: "enum", + enum: ProductSize, + default: ProductSize.STANDARD, + }) + size: ProductSize; + + @Column({ + type: "enum", + enum: StockStatus, + default: StockStatus.OUT_STOCK, + }) + stock_status: StockStatus; + + @ManyToOne(() => Organization, (org) => org.products) + org: Organization; + + @ManyToOne(() => User, (user) => user.products) + user: User; +} diff --git a/src/models/profile.ts b/src/models/profile.ts index 07f61db7..9f96b99a 100644 --- a/src/models/profile.ts +++ b/src/models/profile.ts @@ -1,24 +1,24 @@ -import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from "typeorm"; -import { User } from "./user"; -import ExtendedBaseEntity from "./extended-base-entity"; - -@Entity() -export class Profile extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - first_name: string; - - @Column() - last_name: string; - - @Column({ nullable: true }) - phone_number: string; - - @Column() - avatarUrl: string; - - @OneToOne(() => User, (user) => user.profile) - user: User; -} +import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from "typeorm"; +import { User } from "./user"; +import ExtendedBaseEntity from "./extended-base-entity"; + +@Entity() +export class Profile extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + first_name: string; + + @Column() + last_name: string; + + @Column({ nullable: true }) + phone_number: string; + + @Column() + avatarUrl: string; + + @OneToOne(() => User, (user) => user.profile) + user: User; +} diff --git a/src/models/sms.ts b/src/models/sms.ts index 46be4ff0..d1e09108 100644 --- a/src/models/sms.ts +++ b/src/models/sms.ts @@ -1,57 +1,57 @@ -// import { -// Entity, -// PrimaryGeneratedColumn, -// Column, -// CreateDateColumn, -// ManyToOne, -// } from "typeorm"; -// import ExtendedBaseEntity from "./extended-base-entity"; -// import { User } from "."; - -// @Entity() -// export class Sms extends ExtendedBaseEntity { -// @PrimaryGeneratedColumn("uuid") -// id: string; - -// @Column() -// phone_number: string; - -// @Column() -// message: string; - -// @Column() -// @ManyToOne(() => User, (user) => user.id) -// sender_id: User; - -// @CreateDateColumn() -// createdAt: Date; -// } - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from "typeorm"; -import ExtendedBaseEntity from "./extended-base-entity"; -import { User } from "./user"; - -@Entity() -export class Sms extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - phone_number: string; - - @Column() - message: string; - - @ManyToOne(() => User, (user) => user.sms) - sender: User; - - @CreateDateColumn() - createdAt: Date; -} +// import { +// Entity, +// PrimaryGeneratedColumn, +// Column, +// CreateDateColumn, +// ManyToOne, +// } from "typeorm"; +// import ExtendedBaseEntity from "./extended-base-entity"; +// import { User } from "."; + +// @Entity() +// export class Sms extends ExtendedBaseEntity { +// @PrimaryGeneratedColumn("uuid") +// id: string; + +// @Column() +// phone_number: string; + +// @Column() +// message: string; + +// @Column() +// @ManyToOne(() => User, (user) => user.id) +// sender_id: User; + +// @CreateDateColumn() +// createdAt: Date; +// } + +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from "typeorm"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { User } from "./user"; + +@Entity() +export class Sms extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + phone_number: string; + + @Column() + message: string; + + @ManyToOne(() => User, (user) => user.sms) + sender: User; + + @CreateDateColumn() + createdAt: Date; +} diff --git a/src/models/squeeze.ts b/src/models/squeeze.ts index a81c8e7e..04c328d6 100644 --- a/src/models/squeeze.ts +++ b/src/models/squeeze.ts @@ -1,48 +1,48 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - CreateDateColumn, - UpdateDateColumn, -} from "typeorm"; - -@Entity() -class Squeeze { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column({ unique: true }) - email: string; - - @Column({ nullable: true }) - first_name?: string; - - @Column({ nullable: true }) - last_name?: string; - - @Column({ nullable: true }) - phone?: string; - - @Column({ nullable: true }) - location?: string; - - @Column({ nullable: true }) - job_title?: string; - - @Column({ nullable: true }) - company?: string; - - @Column("simple-array", { nullable: true }) - interests?: string[]; - - @Column({ nullable: true }) - referral_source?: string; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} - -export { Squeeze }; +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, +} from "typeorm"; + +@Entity() +class Squeeze { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ unique: true }) + email: string; + + @Column({ nullable: true }) + first_name?: string; + + @Column({ nullable: true }) + last_name?: string; + + @Column({ nullable: true }) + phone?: string; + + @Column({ nullable: true }) + location?: string; + + @Column({ nullable: true }) + job_title?: string; + + @Column({ nullable: true }) + company?: string; + + @Column("simple-array", { nullable: true }) + interests?: string[]; + + @Column({ nullable: true }) + referral_source?: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} + +export { Squeeze }; diff --git a/src/models/user-organisation.ts b/src/models/user-organisation.ts index de4b67fb..97498dca 100644 --- a/src/models/user-organisation.ts +++ b/src/models/user-organisation.ts @@ -1,31 +1,31 @@ -import { Entity, ManyToOne, Column, PrimaryColumn, JoinColumn } from "typeorm"; -import { User } from "./user"; -import { Organization } from "./organization"; -import { UserRole } from "../enums/userRoles"; -import ExtendedBaseEntity from "./extended-base-entity"; - -@Entity() -export class UserOrganization extends ExtendedBaseEntity { - @PrimaryColumn() - userId: string; - - @PrimaryColumn() - organizationId: string; - - @ManyToOne(() => User, (user) => user.userOrganizations) - @JoinColumn({ name: "userId" }) - user: User; - - @ManyToOne( - () => Organization, - (organization) => organization.userOrganizations, - ) - @JoinColumn({ name: "organizationId" }) - organization: Organization; - - @Column({ - type: "enum", - enum: UserRole, - }) - role: UserRole; -} +import { Entity, ManyToOne, Column, PrimaryColumn, JoinColumn } from "typeorm"; +import { User } from "./user"; +import { Organization } from "./organization"; +import { UserRole } from "../enums/userRoles"; +import ExtendedBaseEntity from "./extended-base-entity"; + +@Entity() +export class UserOrganization extends ExtendedBaseEntity { + @PrimaryColumn() + userId: string; + + @PrimaryColumn() + organizationId: string; + + @ManyToOne(() => User, (user) => user.userOrganizations) + @JoinColumn({ name: "userId" }) + user: User; + + @ManyToOne( + () => Organization, + (organization) => organization.userOrganizations, + ) + @JoinColumn({ name: "organizationId" }) + organization: Organization; + + @Column({ + type: "enum", + enum: UserRole, + }) + role: UserRole; +} diff --git a/src/models/user.ts b/src/models/user.ts index 03dc0d22..aed51201 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -1,157 +1,157 @@ -import { IsEmail } from "class-validator"; -import crypto from "crypto"; -import { - Column, - CreateDateColumn, - DeleteDateColumn, - Entity, - JoinColumn, - JoinTable, - ManyToMany, - OneToMany, - OneToOne, - PrimaryGeneratedColumn, - Unique, - UpdateDateColumn, -} from "typeorm"; -import { - Blog, - Comment, - Organization, - Product, - Profile, - Sms, - Notification, -} from "."; -import { UserRole } from "../enums/userRoles"; -import { getIsInvalidMessage } from "../utils"; -import ExtendedBaseEntity from "./extended-base-entity"; -import { Like } from "./like"; -import { OrganizationMember } from "./organization-member"; -import { UserOrganization } from "./user-organisation"; - -@Entity() -@Unique(["email"]) -export class User extends ExtendedBaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - name: string; - - @Column() - @IsEmail(undefined, { message: getIsInvalidMessage("Email") }) - email: string; - - @Column({ nullable: true }) - password: string; - - @Column({ nullable: true }) - google_id: string; - - @Column({ - default: false, - }) - isverified: boolean; - - @OneToOne(() => Profile, (profile) => profile.user, { cascade: true }) - @JoinColumn() - profile: Profile; - - @Column({ - type: "enum", - enum: UserRole, - default: UserRole.USER, - }) - role: UserRole; - - @Column({ nullable: true }) - otp: number; - - @Column({ nullable: true }) - otp_expires_at: Date; - - @OneToMany(() => Product, (product) => product.user, { cascade: true }) - @JoinTable() - products: Product[]; - - @OneToMany(() => Blog, (blog) => blog.author) - blogs: Blog[]; - - @OneToMany(() => Like, (like) => like.user) - likes: Like[]; - - @OneToMany( - () => UserOrganization, - (userOrganization) => userOrganization.user, - ) - userOrganizations: UserOrganization[]; - - @OneToMany(() => Sms, (sms) => sms.sender, { cascade: true }) - sms: Sms[]; - - @ManyToMany(() => Organization, (organization) => organization.users, { - cascade: true, - }) - @JoinTable() - organizations: Organization[]; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; - - @Column({ type: "boolean", default: false }) - is_deleted: boolean; - - @DeleteDateColumn({ nullable: true }) - deletedAt: Date; - - @Column({ nullable: true }) - passwordResetToken: string; - - @Column({ nullable: true, type: "bigint" }) - passwordResetExpires: number; - - @Column("jsonb", { nullable: true }) - timezone: { - timezone: string; - gmtOffset: string; - description: string; - }; - - @OneToMany( - () => OrganizationMember, - (organizationMember) => organizationMember.organization_id, - ) - organizationMembers: OrganizationMember[]; - - @OneToMany(() => Notification, (notification) => notification.user) - notifications: Notification[]; - - @OneToMany(() => Comment, (comment) => comment.author) - comments: Comment[]; - - @Column({ nullable: true }) - secret: string; - - @Column({ default: false }) - is_2fa_enabled: boolean; - - @Column("simple-array", { nullable: true }) - backup_codes: string[]; - - createPasswordResetToken(): string { - const resetToken = crypto.randomBytes(32).toString("hex"); - - this.passwordResetToken = crypto - .createHash("sha256") - .update(resetToken) - .digest("hex"); - - this.passwordResetExpires = Date.now() + 10 * 60 * 1000; - - return resetToken; - } -} +import { IsEmail } from "class-validator"; +import crypto from "crypto"; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + OneToMany, + OneToOne, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from "typeorm"; +import { + Blog, + Comment, + Organization, + Product, + Profile, + Sms, + Notification, +} from "."; +import { UserRole } from "../enums/userRoles"; +import { getIsInvalidMessage } from "../utils"; +import ExtendedBaseEntity from "./extended-base-entity"; +import { Like } from "./like"; +import { OrganizationMember } from "./organization-member"; +import { UserOrganization } from "./user-organisation"; + +@Entity() +@Unique(["email"]) +export class User extends ExtendedBaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + name: string; + + @Column() + @IsEmail(undefined, { message: getIsInvalidMessage("Email") }) + email: string; + + @Column({ nullable: true }) + password: string; + + @Column({ nullable: true }) + google_id: string; + + @Column({ + default: false, + }) + isverified: boolean; + + @OneToOne(() => Profile, (profile) => profile.user, { cascade: true }) + @JoinColumn() + profile: Profile; + + @Column({ + type: "enum", + enum: UserRole, + default: UserRole.USER, + }) + role: UserRole; + + @Column({ nullable: true }) + otp: number; + + @Column({ nullable: true }) + otp_expires_at: Date; + + @OneToMany(() => Product, (product) => product.user, { cascade: true }) + @JoinTable() + products: Product[]; + + @OneToMany(() => Blog, (blog) => blog.author) + blogs: Blog[]; + + @OneToMany(() => Like, (like) => like.user) + likes: Like[]; + + @OneToMany( + () => UserOrganization, + (userOrganization) => userOrganization.user, + ) + userOrganizations: UserOrganization[]; + + @OneToMany(() => Sms, (sms) => sms.sender, { cascade: true }) + sms: Sms[]; + + @ManyToMany(() => Organization, (organization) => organization.users, { + cascade: true, + }) + @JoinTable() + organizations: Organization[]; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + @Column({ type: "boolean", default: false }) + is_deleted: boolean; + + @DeleteDateColumn({ nullable: true }) + deletedAt: Date; + + @Column({ nullable: true }) + passwordResetToken: string; + + @Column({ nullable: true, type: "bigint" }) + passwordResetExpires: number; + + @Column("jsonb", { nullable: true }) + timezone: { + timezone: string; + gmtOffset: string; + description: string; + }; + + @OneToMany( + () => OrganizationMember, + (organizationMember) => organizationMember.organization_id, + ) + organizationMembers: OrganizationMember[]; + + @OneToMany(() => Notification, (notification) => notification.user) + notifications: Notification[]; + + @OneToMany(() => Comment, (comment) => comment.author) + comments: Comment[]; + + @Column({ nullable: true }) + secret: string; + + @Column({ default: false }) + is_2fa_enabled: boolean; + + @Column("simple-array", { nullable: true }) + backup_codes: string[]; + + createPasswordResetToken(): string { + const resetToken = crypto.randomBytes(32).toString("hex"); + + this.passwordResetToken = crypto + .createHash("sha256") + .update(resetToken) + .digest("hex"); + + this.passwordResetExpires = Date.now() + 10 * 60 * 1000; + + return resetToken; + } +} diff --git a/src/routes/admin.ts b/src/routes/admin.ts index ec618c06..6e07234c 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -1,73 +1,73 @@ -import { Router } from "express"; -import admin from "../controllers/AdminController"; -import { authMiddleware, checkPermissions } from "../middleware"; -import { UserRole } from "../enums/userRoles"; -import { Organization } from "../models"; -import { Limiter } from "../utils"; - -const adminRouter = Router(); - -const adminOrganisationController = new admin.AdminOrganisationController(); -const adminUserController = new admin.AdminUserController(); -const adminLogController = new admin.AdminLogController(); - -// Organisation -adminRouter.patch( - "/admin/organisation/:id", - Limiter, - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN]), - adminOrganisationController.updateOrg.bind(adminOrganisationController), -); - -// Organisation -adminRouter.delete( - "/admin/organizations/:org_id/delete", - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN]), - adminOrganisationController.deleteOrganization.bind( - adminOrganisationController, - ), -); - -// User -adminRouter.get( - "/admin/users", - Limiter, - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN]), - adminUserController.listUsers.bind(adminUserController), -); - -// User -adminRouter.patch( - "/admin/users/:id", - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN]), - adminUserController.updateUser.bind(adminUserController), // Use updateUser method -); - -adminRouter.post( - "/admin/users/:user_id/roles", - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN]), - adminOrganisationController.setUserRole.bind(adminOrganisationController), -); - -adminRouter.get( - "/admin/users/:id", - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN]), - adminUserController.getUserBySuperadmin.bind(adminUserController), -); - -// Logs -adminRouter.get( - "/admin/logs", - Limiter, - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN]), - adminLogController.getLogs.bind(adminLogController), -); - -export { adminRouter }; +import { Router } from "express"; +import admin from "../controllers/AdminController"; +import { authMiddleware, checkPermissions } from "../middleware"; +import { UserRole } from "../enums/userRoles"; +import { Organization } from "../models"; +import { Limiter } from "../utils"; + +const adminRouter = Router(); + +const adminOrganisationController = new admin.AdminOrganisationController(); +const adminUserController = new admin.AdminUserController(); +const adminLogController = new admin.AdminLogController(); + +// Organisation +adminRouter.patch( + "/admin/organisation/:id", + Limiter, + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN]), + adminOrganisationController.updateOrg.bind(adminOrganisationController), +); + +// Organisation +adminRouter.delete( + "/admin/organizations/:org_id/delete", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN]), + adminOrganisationController.deleteOrganization.bind( + adminOrganisationController, + ), +); + +// User +adminRouter.get( + "/admin/users", + Limiter, + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN]), + adminUserController.listUsers.bind(adminUserController), +); + +// User +adminRouter.patch( + "/admin/users/:id", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN]), + adminUserController.updateUser.bind(adminUserController), // Use updateUser method +); + +adminRouter.post( + "/admin/users/:user_id/roles", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN]), + adminOrganisationController.setUserRole.bind(adminOrganisationController), +); + +adminRouter.get( + "/admin/users/:id", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN]), + adminUserController.getUserBySuperadmin.bind(adminUserController), +); + +// Logs +adminRouter.get( + "/admin/logs", + Limiter, + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN]), + adminLogController.getLogs.bind(adminLogController), +); + +export { adminRouter }; diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 2cf1a277..ea6d413d 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -1,56 +1,56 @@ -import { Router } from "express"; -import { - VerifyUserMagicLink, - changePassword, - changeUserRole, - createMagicToken, - enable2FA, - forgotPassword, - googleAuthCall, - login, - resetPassword, - signUp, - verify2FA, - verifyOtp, -} from "../controllers"; -import { UserRole } from "../enums/userRoles"; - -import { authMiddleware, checkPermissions } from "../middleware"; -import { requestBodyValidator } from "../middleware/request-validation"; -import { emailSchema } from "../utils/request-body-validator"; -import { enable2FASchema } from "../schema/auth.schema"; - -const authRoute = Router(); - -authRoute.post("/auth/register", signUp); -authRoute.post("/auth/verify-otp", verifyOtp); -authRoute.post("/auth/login", login); -authRoute.put( - "/auth/organizations/:organization_id/users/:user_id/role", - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]), - changeUserRole, -); - -authRoute.post("/auth/forgot-password", forgotPassword); -authRoute.post("/auth/reset-password/:token", resetPassword); - -authRoute.post("/auth/google", googleAuthCall); - -authRoute.patch("/auth/change-password", authMiddleware, changePassword); - -authRoute.post( - "/auth/magic-link", - requestBodyValidator(emailSchema), - createMagicToken, -); -authRoute.get("/auth/magic-link/verify", VerifyUserMagicLink); -authRoute.post( - "/auth/2fa/enable", - requestBodyValidator(enable2FASchema), - authMiddleware, - enable2FA, -); -authRoute.post("/auth/2fa/verify", authMiddleware, verify2FA); - -export { authRoute }; +import { Router } from "express"; +import { + VerifyUserMagicLink, + changePassword, + changeUserRole, + createMagicToken, + enable2FA, + forgotPassword, + googleAuthCall, + login, + resetPassword, + signUp, + verify2FA, + verifyOtp, +} from "../controllers"; +import { UserRole } from "../enums/userRoles"; + +import { authMiddleware, checkPermissions } from "../middleware"; +import { requestBodyValidator } from "../middleware/request-validation"; +import { emailSchema } from "../utils/request-body-validator"; +import { enable2FASchema } from "../schema/auth.schema"; + +const authRoute = Router(); + +authRoute.post("/auth/register", signUp); +authRoute.post("/auth/verify-otp", verifyOtp); +authRoute.post("/auth/login", login); +authRoute.put( + "/auth/organizations/:organization_id/users/:user_id/role", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]), + changeUserRole, +); + +authRoute.post("/auth/forgot-password", forgotPassword); +authRoute.post("/auth/reset-password/:token", resetPassword); + +authRoute.post("/auth/google", googleAuthCall); + +authRoute.patch("/auth/change-password", authMiddleware, changePassword); + +authRoute.post( + "/auth/magic-link", + requestBodyValidator(emailSchema), + createMagicToken, +); +authRoute.get("/auth/magic-link/verify", VerifyUserMagicLink); +authRoute.post( + "/auth/2fa/enable", + requestBodyValidator(enable2FASchema), + authMiddleware, + enable2FA, +); +authRoute.post("/auth/2fa/verify", authMiddleware, verify2FA); + +export { authRoute }; diff --git a/src/routes/billing-plans.ts b/src/routes/billing-plans.ts index 36f0804c..0a313979 100644 --- a/src/routes/billing-plans.ts +++ b/src/routes/billing-plans.ts @@ -1,15 +1,15 @@ -import { Router } from "express"; -import { BillingController } from "../controllers"; -import { authMiddleware } from "../middleware"; - -const billingRouter = Router(); - -const billingController = new BillingController(); - -billingRouter.get( - "/billing-plans", - authMiddleware, - billingController.getAllBillings, -); - -export { billingRouter }; +import { Router } from "express"; +import { BillingController } from "../controllers"; +import { authMiddleware } from "../middleware"; + +const billingRouter = Router(); + +const billingController = new BillingController(); + +billingRouter.get( + "/billing-plans", + authMiddleware, + billingController.getAllBillings, +); + +export { billingRouter }; diff --git a/src/routes/billingplan.ts b/src/routes/billingplan.ts index c996d539..8744eb93 100644 --- a/src/routes/billingplan.ts +++ b/src/routes/billingplan.ts @@ -1,23 +1,23 @@ -import { Router } from "express"; -import { BillingPlanController } from "../controllers"; -import { authMiddleware, checkPermissions } from "../middleware"; -import { UserRole } from "../enums/userRoles"; - -const billingPlanRouter = Router(); -const billingPlanController = new BillingPlanController(); - -billingPlanRouter.post( - "/billing-plans", - authMiddleware, - checkPermissions([UserRole.ADMIN, UserRole.SUPER_ADMIN]), - billingPlanController.createBillingPlan, -); - -billingPlanRouter.get( - "/billing-plans/:id", - authMiddleware, - checkPermissions([UserRole.ADMIN, UserRole.SUPER_ADMIN]), - billingPlanController.getBillingPlans, -); - -export { billingPlanRouter }; +import { Router } from "express"; +import { BillingPlanController } from "../controllers"; +import { authMiddleware, checkPermissions } from "../middleware"; +import { UserRole } from "../enums/userRoles"; + +const billingPlanRouter = Router(); +const billingPlanController = new BillingPlanController(); + +billingPlanRouter.post( + "/billing-plans", + authMiddleware, + checkPermissions([UserRole.ADMIN, UserRole.SUPER_ADMIN]), + billingPlanController.createBillingPlan, +); + +billingPlanRouter.get( + "/billing-plans/:id", + authMiddleware, + checkPermissions([UserRole.ADMIN, UserRole.SUPER_ADMIN]), + billingPlanController.getBillingPlans, +); + +export { billingPlanRouter }; diff --git a/src/routes/contact.ts b/src/routes/contact.ts index 7c50c7ed..372ad3a4 100644 --- a/src/routes/contact.ts +++ b/src/routes/contact.ts @@ -1,17 +1,17 @@ -import { Router } from "express"; -import { ContactController } from "../controllers/contactController"; - -const contactRouter = Router(); -const contactController = new ContactController(); - -contactRouter.post( - "/contact", - contactController.createContact.bind(contactController), -); - -contactRouter.get( - "/contact", - contactController.getAllContact.bind(contactController), -); - -export { contactRouter }; +import { Router } from "express"; +import { ContactController } from "../controllers/contactController"; + +const contactRouter = Router(); +const contactController = new ContactController(); + +contactRouter.post( + "/contact", + contactController.createContact.bind(contactController), +); + +contactRouter.get( + "/contact", + contactController.getAllContact.bind(contactController), +); + +export { contactRouter }; diff --git a/src/routes/export.ts b/src/routes/export.ts index 956bb24c..ead04ad4 100644 --- a/src/routes/export.ts +++ b/src/routes/export.ts @@ -1,16 +1,16 @@ -import { Router } from "express"; -import exportController from "../controllers/exportController"; -import { authMiddleware } from "../middleware"; -import { checkPermissions } from "../middleware/checkUserRole"; -import { UserRole } from "../enums/userRoles"; - -const exportRouter = Router(); - -exportRouter.get( - "/organisation/members/export", - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN, UserRole.USER]), - exportController.exportData, -); - -export { exportRouter }; +import { Router } from "express"; +import exportController from "../controllers/exportController"; +import { authMiddleware } from "../middleware"; +import { checkPermissions } from "../middleware/checkUserRole"; +import { UserRole } from "../enums/userRoles"; + +const exportRouter = Router(); + +exportRouter.get( + "/organisation/members/export", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN, UserRole.USER]), + exportController.exportData, +); + +export { exportRouter }; diff --git a/src/routes/faq.ts b/src/routes/faq.ts index 57db08e3..df86420b 100644 --- a/src/routes/faq.ts +++ b/src/routes/faq.ts @@ -1,19 +1,19 @@ -import { Router } from "express"; -import { FAQController } from "../controllers/FaqController"; -import { authMiddleware, checkPermissions } from "../middleware"; -import { UserRole } from "../enums/userRoles"; - -const faqRouter = Router(); -const faqController = new FAQController(); - -faqRouter.post("/faqs", authMiddleware, faqController.createFAQ); -faqRouter.patch("/faqs/:id", authMiddleware, faqController.updateFaq); -faqRouter.get("/faqs", faqController.getFaq); -faqRouter.delete( - "/faqs/:faqId", - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN]), - faqController.deleteFaq, -); - -export { faqRouter }; +import { Router } from "express"; +import { FAQController } from "../controllers/FaqController"; +import { authMiddleware, checkPermissions } from "../middleware"; +import { UserRole } from "../enums/userRoles"; + +const faqRouter = Router(); +const faqController = new FAQController(); + +faqRouter.post("/faqs", authMiddleware, faqController.createFAQ); +faqRouter.patch("/faqs/:id", authMiddleware, faqController.updateFaq); +faqRouter.get("/faqs", faqController.getFaq); +faqRouter.delete( + "/faqs/:faqId", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN]), + faqController.deleteFaq, +); + +export { faqRouter }; diff --git a/src/routes/help-center.ts b/src/routes/help-center.ts index 8d40d090..df96b486 100644 --- a/src/routes/help-center.ts +++ b/src/routes/help-center.ts @@ -1,34 +1,34 @@ -// src/routes/help-center.ts -import { Router } from "express"; -import HelpController from "../controllers/HelpController"; -import { authMiddleware } from "../middleware/auth"; -import { verifyAdmin } from "../services"; - -const helpRouter = Router(); -const helpController = new HelpController(); -helpRouter.post( - "/help-center/topics", - authMiddleware, - helpController.createTopic.bind(helpController), -); -helpRouter.patch( - "/help-center/topics/:id", - authMiddleware, - helpController.updateTopic.bind(helpController), -); -helpRouter.get( - "/help-center/topics", - authMiddleware, - helpController.getAllTopics.bind(helpController), -); -helpRouter.get( - "/help-center/topics/:id", - authMiddleware, - helpController.getTopicById.bind(helpController), -); -helpRouter.delete( - "/help-center/topics/:id", - authMiddleware, - helpController.deleteTopic.bind(helpController), -); -export { helpRouter }; +// src/routes/help-center.ts +import { Router } from "express"; +import HelpController from "../controllers/HelpController"; +import { authMiddleware } from "../middleware/auth"; +import { verifyAdmin } from "../services"; + +const helpRouter = Router(); +const helpController = new HelpController(); +helpRouter.post( + "/help-center/topics", + authMiddleware, + helpController.createTopic.bind(helpController), +); +helpRouter.patch( + "/help-center/topics/:id", + authMiddleware, + helpController.updateTopic.bind(helpController), +); +helpRouter.get( + "/help-center/topics", + authMiddleware, + helpController.getAllTopics.bind(helpController), +); +helpRouter.get( + "/help-center/topics/:id", + authMiddleware, + helpController.getTopicById.bind(helpController), +); +helpRouter.delete( + "/help-center/topics/:id", + authMiddleware, + helpController.deleteTopic.bind(helpController), +); +export { helpRouter }; diff --git a/src/routes/newsLetterSubscription.ts b/src/routes/newsLetterSubscription.ts index b64af90a..e992ca94 100644 --- a/src/routes/newsLetterSubscription.ts +++ b/src/routes/newsLetterSubscription.ts @@ -1,30 +1,30 @@ -import { Router } from "express"; -import { - getAllNewsletter, - subscribeToNewsletter, - unSubscribeToNewsletter, -} from "../controllers/NewsLetterSubscriptionController"; -import { UserRole } from "../enums/userRoles"; -import { authMiddleware, checkPermissions } from "../middleware"; - -const newsLetterSubscriptionRoute = Router(); - -newsLetterSubscriptionRoute.post( - "/newsletter-subscription", - authMiddleware, - subscribeToNewsletter, -); - -newsLetterSubscriptionRoute.post( - "/newsletter-subscription/unsubscribe", - authMiddleware, - unSubscribeToNewsletter, -); - -newsLetterSubscriptionRoute.get( - "/newsletter-subscription", - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN]), - getAllNewsletter, -); -export { newsLetterSubscriptionRoute }; +import { Router } from "express"; +import { + getAllNewsletter, + subscribeToNewsletter, + unSubscribeToNewsletter, +} from "../controllers/NewsLetterSubscriptionController"; +import { UserRole } from "../enums/userRoles"; +import { authMiddleware, checkPermissions } from "../middleware"; + +const newsLetterSubscriptionRoute = Router(); + +newsLetterSubscriptionRoute.post( + "/newsletter-subscription", + authMiddleware, + subscribeToNewsletter, +); + +newsLetterSubscriptionRoute.post( + "/newsletter-subscription/unsubscribe", + authMiddleware, + unSubscribeToNewsletter, +); + +newsLetterSubscriptionRoute.get( + "/newsletter-subscription", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN]), + getAllNewsletter, +); +export { newsLetterSubscriptionRoute }; diff --git a/src/routes/notification.ts b/src/routes/notification.ts index 0e4ab9e9..6f5dd7e3 100644 --- a/src/routes/notification.ts +++ b/src/routes/notification.ts @@ -1,14 +1,14 @@ -import { Router } from "express"; -import { NotificationController } from "../controllers"; -import { authMiddleware } from "../middleware"; - -const notificationRouter = Router(); -const notificationsController = new NotificationController(); - -notificationRouter.get( - "/notifications/all", - authMiddleware, - notificationsController.getNotificationsForUser, -); - -export { notificationRouter }; +import { Router } from "express"; +import { NotificationController } from "../controllers"; +import { authMiddleware } from "../middleware"; + +const notificationRouter = Router(); +const notificationsController = new NotificationController(); + +notificationRouter.get( + "/notifications/all", + authMiddleware, + notificationsController.getNotificationsForUser, +); + +export { notificationRouter }; diff --git a/src/routes/notificationsettings.ts b/src/routes/notificationsettings.ts index 173810e9..f82181db 100644 --- a/src/routes/notificationsettings.ts +++ b/src/routes/notificationsettings.ts @@ -1,18 +1,18 @@ -import { CreateOrUpdateNotification, GetNotification } from "../controllers"; -import { Router } from "express"; -import { authMiddleware } from "../middleware"; - -const notificationsettingsRouter = Router(); - -notificationsettingsRouter.put( - "/settings/notification-settings", - authMiddleware, - CreateOrUpdateNotification, -); -notificationsettingsRouter.get( - "/settings/notification-settings/:user_id", - authMiddleware, - GetNotification, -); - -export { notificationsettingsRouter }; +import { CreateOrUpdateNotification, GetNotification } from "../controllers"; +import { Router } from "express"; +import { authMiddleware } from "../middleware"; + +const notificationsettingsRouter = Router(); + +notificationsettingsRouter.put( + "/settings/notification-settings", + authMiddleware, + CreateOrUpdateNotification, +); +notificationsettingsRouter.get( + "/settings/notification-settings/:user_id", + authMiddleware, + GetNotification, +); + +export { notificationsettingsRouter }; diff --git a/src/routes/organisation.ts b/src/routes/organisation.ts index 5e03e9b7..0ac44ebc 100644 --- a/src/routes/organisation.ts +++ b/src/routes/organisation.ts @@ -1,108 +1,108 @@ -import Router from "express"; -import { OrgController } from "../controllers/OrgController"; -import { UserRole } from "../enums/userRoles"; -import { - authMiddleware, - checkPermissions, - organizationValidation, - validateOrgId, - validateOrgRole, - validateUpdateOrg, -} from "../middleware"; - -const orgRouter = Router(); -const orgController = new OrgController(); - -orgRouter.get( - "/organizations/invites", - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]), - orgController.getAllInvite.bind(orgController), -); -orgRouter.get( - "/organizations/:org_id", - authMiddleware, - validateOrgId, - orgController.getSingleOrg.bind(orgController), -); -orgRouter.delete( - "/organizations/:org_id/user/:user_id", - authMiddleware, - validateOrgId, - orgController.removeUser.bind(orgController), -); - -orgRouter.post( - "/organizations", - authMiddleware, - organizationValidation, - orgController.createOrganisation.bind(orgController), -); - -orgRouter.get( - "/organizations/:org_id/invite", - authMiddleware, - checkPermissions([UserRole.ADMIN, UserRole.SUPER_ADMIN]), - orgController.generateGenericInviteLink.bind(orgController), -); - -orgRouter.post( - "organizations/:org_id/roles", - authMiddleware, - validateOrgRole, - checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]), - orgController.createOrganizationRole.bind(orgController), -); - -orgRouter.post( - "/organizations/:org_id/send-invite", - authMiddleware, - checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]), - orgController.generateAndSendInviteLinks.bind(orgController), -); -orgRouter.post( - "/organizations/accept-invite", - authMiddleware, - orgController.addUserToOrganizationWithInvite.bind(orgController), -); - -orgRouter.get( - "/users/:id/organizations", - authMiddleware, - orgController.getOrganizations.bind(orgController), -); - -orgRouter.get( - "/members/search", - authMiddleware, - orgController.searchOrganizationMembers.bind(orgController), -); - -orgRouter.put( - "/organizations/:organization_id", - authMiddleware, - validateUpdateOrg, - checkPermissions([UserRole.SUPER_ADMIN, UserRole.USER]), - orgController.updateOrganisation.bind(orgController), -); - -orgRouter.get( - "/organizations/:org_id/roles/:role_id", - authMiddleware, - orgController.getSingleRole.bind(orgController), -); - -orgRouter.get( - "/organizations/:org_id/roles", - authMiddleware, - orgController.getAllOrganizationRoles.bind(orgController), -); - -orgRouter.put( - "/organizations/:org_id/roles/:role_id/permissions", - authMiddleware, - checkPermissions([UserRole.ADMIN]), - orgController.updateOrganizationRolePermissions.bind(orgController), -); - -export { orgRouter }; +import Router from "express"; +import { OrgController } from "../controllers/OrgController"; +import { UserRole } from "../enums/userRoles"; +import { + authMiddleware, + checkPermissions, + organizationValidation, + validateOrgId, + validateOrgRole, + validateUpdateOrg, +} from "../middleware"; + +const orgRouter = Router(); +const orgController = new OrgController(); + +orgRouter.get( + "/organizations/invites", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]), + orgController.getAllInvite.bind(orgController), +); +orgRouter.get( + "/organizations/:org_id", + authMiddleware, + validateOrgId, + orgController.getSingleOrg.bind(orgController), +); +orgRouter.delete( + "/organizations/:org_id/user/:user_id", + authMiddleware, + validateOrgId, + orgController.removeUser.bind(orgController), +); + +orgRouter.post( + "/organizations", + authMiddleware, + organizationValidation, + orgController.createOrganisation.bind(orgController), +); + +orgRouter.get( + "/organizations/:org_id/invite", + authMiddleware, + checkPermissions([UserRole.ADMIN, UserRole.SUPER_ADMIN]), + orgController.generateGenericInviteLink.bind(orgController), +); + +orgRouter.post( + "organizations/:org_id/roles", + authMiddleware, + validateOrgRole, + checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]), + orgController.createOrganizationRole.bind(orgController), +); + +orgRouter.post( + "/organizations/:org_id/send-invite", + authMiddleware, + checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]), + orgController.generateAndSendInviteLinks.bind(orgController), +); +orgRouter.post( + "/organizations/accept-invite", + authMiddleware, + orgController.addUserToOrganizationWithInvite.bind(orgController), +); + +orgRouter.get( + "/users/:id/organizations", + authMiddleware, + orgController.getOrganizations.bind(orgController), +); + +orgRouter.get( + "/members/search", + authMiddleware, + orgController.searchOrganizationMembers.bind(orgController), +); + +orgRouter.put( + "/organizations/:organization_id", + authMiddleware, + validateUpdateOrg, + checkPermissions([UserRole.SUPER_ADMIN, UserRole.USER]), + orgController.updateOrganisation.bind(orgController), +); + +orgRouter.get( + "/organizations/:org_id/roles/:role_id", + authMiddleware, + orgController.getSingleRole.bind(orgController), +); + +orgRouter.get( + "/organizations/:org_id/roles", + authMiddleware, + orgController.getAllOrganizationRoles.bind(orgController), +); + +orgRouter.put( + "/organizations/:org_id/roles/:role_id/permissions", + authMiddleware, + checkPermissions([UserRole.ADMIN]), + orgController.updateOrganizationRolePermissions.bind(orgController), +); + +export { orgRouter }; diff --git a/src/routes/payment.ts b/src/routes/payment.ts index daaf9e38..d98219af 100644 --- a/src/routes/payment.ts +++ b/src/routes/payment.ts @@ -1,18 +1,18 @@ -import { Router } from "express"; -import { PaymentController } from "../controllers"; -import { authMiddleware } from "../middleware"; - -const paymentFlutterwaveRouter = Router(); - -paymentFlutterwaveRouter.post( - "/payments/flutterwave/initiate", - authMiddleware, - PaymentController.initiatePayment, -); -paymentFlutterwaveRouter.get( - "/payments/flutterwave/verify/:transactionId", - authMiddleware, - PaymentController.verifyPayment, -); - -export { paymentFlutterwaveRouter }; +import { Router } from "express"; +import { PaymentController } from "../controllers"; +import { authMiddleware } from "../middleware"; + +const paymentFlutterwaveRouter = Router(); + +paymentFlutterwaveRouter.post( + "/payments/flutterwave/initiate", + authMiddleware, + PaymentController.initiatePayment, +); +paymentFlutterwaveRouter.get( + "/payments/flutterwave/verify/:transactionId", + authMiddleware, + PaymentController.verifyPayment, +); + +export { paymentFlutterwaveRouter }; diff --git a/src/routes/paymentLemonSqueezy.ts b/src/routes/paymentLemonSqueezy.ts index 69e07c53..4903ad86 100644 --- a/src/routes/paymentLemonSqueezy.ts +++ b/src/routes/paymentLemonSqueezy.ts @@ -1,21 +1,21 @@ -import { Router } from "express"; -import { - makePaymentLemonSqueezy, - LemonSqueezyWebhook, -} from "../controllers/PaymentLemonSqueezyController"; -import { authMiddleware } from "../middleware/auth"; -import BodyParser from "body-parser"; -const paymentRouter = Router(); - -paymentRouter.get( - "/payments/lemonsqueezy/initiate", - authMiddleware, - makePaymentLemonSqueezy, -); -paymentRouter.post( - "/payments/lemonsqueezy/webhook", - BodyParser.text({ type: "*/*" }), - LemonSqueezyWebhook, -); - -export { paymentRouter }; +import { Router } from "express"; +import { + makePaymentLemonSqueezy, + LemonSqueezyWebhook, +} from "../controllers/PaymentLemonSqueezyController"; +import { authMiddleware } from "../middleware/auth"; +import BodyParser from "body-parser"; +const paymentRouter = Router(); + +paymentRouter.get( + "/payments/lemonsqueezy/initiate", + authMiddleware, + makePaymentLemonSqueezy, +); +paymentRouter.post( + "/payments/lemonsqueezy/webhook", + BodyParser.text({ type: "*/*" }), + LemonSqueezyWebhook, +); + +export { paymentRouter }; diff --git a/src/routes/paymentStripe.ts b/src/routes/paymentStripe.ts index 87bc1ff8..e8d71eed 100644 --- a/src/routes/paymentStripe.ts +++ b/src/routes/paymentStripe.ts @@ -1,18 +1,18 @@ -/** - * main routes for paymentStripe - */ -import { Router } from "express"; -import { createPaymentIntentStripe } from "../controllers/paymentStripeController"; -import { validatePaymentRequest } from "../middleware/paymentStripeValidation"; -import { authMiddleware } from "../middleware/auth"; - -const paymentStripeRouter = Router(); - -paymentStripeRouter.post( - "/payments/stripe/initiate", - validatePaymentRequest, - validatePaymentRequest, - createPaymentIntentStripe, -); - -export { paymentStripeRouter }; +/** + * main routes for paymentStripe + */ +import { Router } from "express"; +import { createPaymentIntentStripe } from "../controllers/paymentStripeController"; +import { validatePaymentRequest } from "../middleware/paymentStripeValidation"; +import { authMiddleware } from "../middleware/auth"; + +const paymentStripeRouter = Router(); + +paymentStripeRouter.post( + "/payments/stripe/initiate", + validatePaymentRequest, + validatePaymentRequest, + createPaymentIntentStripe, +); + +export { paymentStripeRouter }; diff --git a/src/routes/product.ts b/src/routes/product.ts index a0a03110..a6b9e253 100644 --- a/src/routes/product.ts +++ b/src/routes/product.ts @@ -1,41 +1,46 @@ -import { Router } from "express"; -import { ProductController } from "../controllers/ProductController"; -import { authMiddleware, validOrgAdmin } from "../middleware"; -import { validateProductDetails } from "../middleware/product"; -import { validateUserToOrg } from "../middleware/organizationValidation"; -import { adminOnly } from "../middleware"; - -const productRouter = Router(); -const productController = new ProductController(); - -// route -productRouter.post( - "/organizations/:org_id/products", - validateProductDetails, - authMiddleware, - validOrgAdmin, - productController.createProduct, -); - -productRouter.get( - "/organizations/:org_id/products/search", - authMiddleware, - validateUserToOrg, - productController.getProduct, -); - -productRouter.delete( - "/organizations/:org_id/products/:product_id", - authMiddleware, - validOrgAdmin, - productController.deleteProduct, -); - -productRouter.get( - "/organizations/:org_id/products/:product_id", - authMiddleware, - validOrgAdmin, - productController.getSingleProduct, -); - -export { productRouter }; +import { Router } from "express"; +import { ProductController } from "../controllers/ProductController"; +import { authMiddleware, validOrgAdmin } from "../middleware"; +import { validateProductDetails } from "../middleware/product"; +import { validateUserToOrg } from "../middleware/organizationValidation"; +import { adminOnly } from "../middleware"; + +const productRouter = Router(); +const productController = new ProductController(); + +// route +productRouter.post( + "/organizations/:org_id/products", + validateProductDetails, + authMiddleware, + validOrgAdmin, + productController.createProduct, +); + +productRouter.get( + "/organizations/:org_id/products/search", + authMiddleware, + validateUserToOrg, + productController.getProduct, +); + +productRouter.delete( + "/organizations/:org_id/products/:product_id", + authMiddleware, + validOrgAdmin, + productController.deleteProduct, +); + +const updateRoute = "/:org_id/products/:product_id"; +productRouter + .route(updateRoute) + .patch(authMiddleware, validateUserToOrg, productController.updateProduct); + +productRouter.get( + "/organizations/:org_id/products/:product_id", + authMiddleware, + validOrgAdmin, + productController.getSingleProduct, +); + +export { productRouter }; diff --git a/src/routes/roles.ts b/src/routes/roles.ts index ebb876e2..c1e93f3b 100644 --- a/src/routes/roles.ts +++ b/src/routes/roles.ts @@ -1,9 +1,9 @@ -import { Router } from "express"; -import { authMiddleware } from "../middleware"; -import { createUserRole } from "../controllers"; - -const roleRouter = Router(); - -roleRouter.post("/roles", authMiddleware, createUserRole); - -export { roleRouter }; +import { Router } from "express"; +import { authMiddleware } from "../middleware"; +import { createUserRole } from "../controllers"; + +const roleRouter = Router(); + +roleRouter.post("/roles", authMiddleware, createUserRole); + +export { roleRouter }; diff --git a/src/routes/run-test.ts b/src/routes/run-test.ts index 2bd419ec..4fec2196 100644 --- a/src/routes/run-test.ts +++ b/src/routes/run-test.ts @@ -1,8 +1,8 @@ -import { Router } from "express"; -import { runTestController } from "../controllers"; - -const runTestRouter = Router(); - -runTestRouter.get("/", runTestController); - -export { runTestRouter }; +import { Router } from "express"; +import { runTestController } from "../controllers"; + +const runTestRouter = Router(); + +runTestRouter.get("/", runTestController); + +export { runTestRouter }; diff --git a/src/routes/sendEmail.route.ts b/src/routes/sendEmail.route.ts index e8e47292..3d7d90bd 100644 --- a/src/routes/sendEmail.route.ts +++ b/src/routes/sendEmail.route.ts @@ -1,13 +1,13 @@ -import { Router } from "express"; -import { - SendEmail, - getEmailTemplates, -} from "../controllers/sendEmail.controller"; -import { authMiddleware } from "../middleware"; - -const sendEmailRoute = Router(); - -sendEmailRoute.post("/send-email", authMiddleware, SendEmail); -sendEmailRoute.get("/email-templates", authMiddleware, getEmailTemplates); - -export { sendEmailRoute }; +import { Router } from "express"; +import { + SendEmail, + getEmailTemplates, +} from "../controllers/sendEmail.controller"; +import { authMiddleware } from "../middleware"; + +const sendEmailRoute = Router(); + +sendEmailRoute.post("/send-email", authMiddleware, SendEmail); +sendEmailRoute.get("/email-templates", authMiddleware, getEmailTemplates); + +export { sendEmailRoute }; diff --git a/src/routes/sms.ts b/src/routes/sms.ts index ab37ad38..2c651bd0 100644 --- a/src/routes/sms.ts +++ b/src/routes/sms.ts @@ -1,9 +1,9 @@ -import { Router } from "express"; -import { sendSms } from "../controllers/SmsController"; -import { authMiddleware } from "../middleware"; - -const smsRouter = Router(); - -smsRouter.post("/sms/send", authMiddleware, sendSms); - -export { smsRouter }; +import { Router } from "express"; +import { sendSms } from "../controllers/SmsController"; +import { authMiddleware } from "../middleware"; + +const smsRouter = Router(); + +smsRouter.post("/sms/send", authMiddleware, sendSms); + +export { smsRouter }; diff --git a/src/routes/testimonial.ts b/src/routes/testimonial.ts index 55cdc57f..ce564e2d 100644 --- a/src/routes/testimonial.ts +++ b/src/routes/testimonial.ts @@ -1,34 +1,34 @@ -// src/routes/user.ts -import { Router } from "express"; -import TestimonialsController from "../controllers/TestimonialsController"; -import { authMiddleware } from "../middleware"; -import { validateTestimonial } from "../middleware/testimonial.validation"; - -const testimonialRoute = Router(); -const testimonialController = new TestimonialsController(); - -testimonialRoute.post( - "/testimonials", - authMiddleware, - validateTestimonial, - testimonialController.createTestimonial.bind(testimonialController) -); -testimonialRoute.get( - "/testimonials/:testimonial_id", - authMiddleware, - testimonialController.getTestimonial.bind(testimonialController) -); - -// CODE BY TOMILOLA OLUWAFEMI -testimonialRoute.get( - "/testimonials", - authMiddleware, - testimonialController.getAllTestimonials.bind(testimonialController) -); -testimonialRoute.delete( - "/testimonials/:testimonial_id", - authMiddleware, - testimonialController.deleteTestimonial.bind(testimonialController) -); - -export { testimonialRoute }; +// src/routes/user.ts +import { Router } from "express"; +import TestimonialsController from "../controllers/TestimonialsController"; +import { authMiddleware } from "../middleware"; +import { validateTestimonial } from "../middleware/testimonial.validation"; + +const testimonialRoute = Router(); +const testimonialController = new TestimonialsController(); + +testimonialRoute.post( + "/testimonials", + authMiddleware, + validateTestimonial, + testimonialController.createTestimonial.bind(testimonialController) +); +testimonialRoute.get( + "/testimonials/:testimonial_id", + authMiddleware, + testimonialController.getTestimonial.bind(testimonialController) +); + +// CODE BY TOMILOLA OLUWAFEMI +testimonialRoute.get( + "/testimonials", + authMiddleware, + testimonialController.getAllTestimonials.bind(testimonialController) +); +testimonialRoute.delete( + "/testimonials/:testimonial_id", + authMiddleware, + testimonialController.deleteTestimonial.bind(testimonialController) +); + +export { testimonialRoute }; diff --git a/src/schema/auth.schema.ts b/src/schema/auth.schema.ts index 961e046b..f6557a96 100644 --- a/src/schema/auth.schema.ts +++ b/src/schema/auth.schema.ts @@ -1,47 +1,47 @@ -import { object, string, TypeOf, z } from "zod"; -import { emailSchema } from "../utils/request-body-validator"; - -/** - * @openapi - * components: - * schemas: - * CreateMagicLink: - * type: object - * required: - * - email - * properties: - * email: - * type: string - * format: email - * example: user@example.com - * ValidateMagicLink: - * type: object - * required: - * - token - * properties: - * token: - * type: string - * example: "exampleToken123" - */ - -const createMagicLinkPayload = { - body: object({ - email: emailSchema, - }), -}; - -const magicLinkQuery = object({ - token: string().min(1, "Token is required"), -}); - -export const validateMagicLinkSchema = object({ - query: magicLinkQuery, -}); - -export const createMagicLinkSchema = object({ ...createMagicLinkPayload }); -export const enable2FASchema = z.object({ - password: z.string(), -}); - -export type validateMagicLinkInput = TypeOf; -export type CreateMagicLinkInput = TypeOf; +import { object, string, TypeOf, z } from "zod"; +import { emailSchema } from "../utils/request-body-validator"; + +/** + * @openapi + * components: + * schemas: + * CreateMagicLink: + * type: object + * required: + * - email + * properties: + * email: + * type: string + * format: email + * example: user@example.com + * ValidateMagicLink: + * type: object + * required: + * - token + * properties: + * token: + * type: string + * example: "exampleToken123" + */ + +const createMagicLinkPayload = { + body: object({ + email: emailSchema, + }), +}; + +const magicLinkQuery = object({ + token: string().min(1, "Token is required"), +}); + +export const validateMagicLinkSchema = object({ + query: magicLinkQuery, +}); + +export const createMagicLinkSchema = object({ ...createMagicLinkPayload }); +export const enable2FASchema = z.object({ + password: z.string(), +}); + +export type validateMagicLinkInput = TypeOf; +export type CreateMagicLinkInput = TypeOf; diff --git a/src/schema/blog.schema.ts b/src/schema/blog.schema.ts index 7e67e05a..7a06a530 100644 --- a/src/schema/blog.schema.ts +++ b/src/schema/blog.schema.ts @@ -1,93 +1,93 @@ -import { boolean, number, object, string, TypeOf } from "zod"; - -/** - * @openapi - * components: - * schemas: - * Blog: - * type: object - * required: - * - title - * - content - * properties: - * title: - * type: string - * default: "blog title 1" - * content: - * type: string - * default: "Blog wey make sense" - * author: - * type: string - * default: John Doe - * published_at: - * type: string - * format: date-time - * default: 2023-07-21T19:58:00.000Z - * BlogResponse: - * type: object - * properties: - * name: - * type: string - * description: - * type: string - * price: - * type: number - * category: - * type: string - */ - -const payload = { - body: object({ - title: string({ - required_error: "title is required", - }), - content: string({ - required_error: "content is required", - }), - }), -}; - -const paginationSchema = object({ - total_items: number(), - total_pages: number(), - current_page: number(), -}); - -const params = { - params: object({ - blogId: string({ - required_error: "boogId is required", - }), - }), -}; - -export const createBlogSchema = object({ - ...payload, -}); - -export const updateBlogSchema = object({ - ...payload, - ...params, -}); - -export const deleteBlogSchema = object({ - ...params, -}); - -export const getBlogSchema = object({ - ...params, -}); - -export const getAllBlogSchema = object({ - success: boolean(), - message: string(), - status_code: number(), - pagination: paginationSchema, - ...payload, -}); - -export type CreateBlogInput = TypeOf; -export type UpdateBlogInput = TypeOf; -export type ReadBlogInput = TypeOf; -export type DeleteBlogInput = TypeOf; -export type GetAllBlogsResponse = TypeOf; +import { boolean, number, object, string, TypeOf } from "zod"; + +/** + * @openapi + * components: + * schemas: + * Blog: + * type: object + * required: + * - title + * - content + * properties: + * title: + * type: string + * default: "blog title 1" + * content: + * type: string + * default: "Blog wey make sense" + * author: + * type: string + * default: John Doe + * published_at: + * type: string + * format: date-time + * default: 2023-07-21T19:58:00.000Z + * BlogResponse: + * type: object + * properties: + * name: + * type: string + * description: + * type: string + * price: + * type: number + * category: + * type: string + */ + +const payload = { + body: object({ + title: string({ + required_error: "title is required", + }), + content: string({ + required_error: "content is required", + }), + }), +}; + +const paginationSchema = object({ + total_items: number(), + total_pages: number(), + current_page: number(), +}); + +const params = { + params: object({ + blogId: string({ + required_error: "boogId is required", + }), + }), +}; + +export const createBlogSchema = object({ + ...payload, +}); + +export const updateBlogSchema = object({ + ...payload, + ...params, +}); + +export const deleteBlogSchema = object({ + ...params, +}); + +export const getBlogSchema = object({ + ...params, +}); + +export const getAllBlogSchema = object({ + success: boolean(), + message: string(), + status_code: number(), + pagination: paginationSchema, + ...payload, +}); + +export type CreateBlogInput = TypeOf; +export type UpdateBlogInput = TypeOf; +export type ReadBlogInput = TypeOf; +export type DeleteBlogInput = TypeOf; +export type GetAllBlogsResponse = TypeOf; diff --git a/src/schema/emailTemplate.ts b/src/schema/emailTemplate.ts index ad8e5c5c..c693dea3 100644 --- a/src/schema/emailTemplate.ts +++ b/src/schema/emailTemplate.ts @@ -1,80 +1,80 @@ -export const emailTTemplateSchema = { - type: "object", - properties: { - id: { - type: "integer", - format: "int64", - example: 6, - }, - title: { - type: "string", - format: "title", - example: "Welome", - }, - description: { - type: "string", - format: "details", - example: "Welcome to our organisation", - }, - created_at: { - type: "string", - format: "date-time", - example: "2019-10-12T07:20:50.52Z", - }, - updated_at: { - type: "string", - format: "date-time", - example: "2022-10-12T07:20:50.52Z", - }, - }, -}; - -export const emailTemplatePaths = { - "/emailTemplates": { - get: { - tags: ["emailTemplates"], - summary: "Get all email templates", - responses: { - "200": { - description: "The list of email templates", - content: { - "application/json": { - schema: { - type: "array", - items: { - $ref: "#/components/schemas/EmailTemplates", - }, - }, - }, - }, - }, - }, - }, - post: { - tags: ["emailTemplates"], - summary: "Create a new email template", - requestBody: { - required: true, - content: { - "application/json": { - schema: { - $ref: "#/components/schemas/EmailTemplates", - }, - }, - }, - }, - responses: { - "200": { - description: "The email template was successfully created", - content: { - "application/json": { - schema: { - $ref: "#/components/schemas/EmailTemplates", - }, - }, - }, - }, - }, - }, - }, -}; +export const emailTTemplateSchema = { + type: "object", + properties: { + id: { + type: "integer", + format: "int64", + example: 6, + }, + title: { + type: "string", + format: "title", + example: "Welome", + }, + description: { + type: "string", + format: "details", + example: "Welcome to our organisation", + }, + created_at: { + type: "string", + format: "date-time", + example: "2019-10-12T07:20:50.52Z", + }, + updated_at: { + type: "string", + format: "date-time", + example: "2022-10-12T07:20:50.52Z", + }, + }, +}; + +export const emailTemplatePaths = { + "/emailTemplates": { + get: { + tags: ["emailTemplates"], + summary: "Get all email templates", + responses: { + "200": { + description: "The list of email templates", + content: { + "application/json": { + schema: { + type: "array", + items: { + $ref: "#/components/schemas/EmailTemplates", + }, + }, + }, + }, + }, + }, + }, + post: { + tags: ["emailTemplates"], + summary: "Create a new email template", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/EmailTemplates", + }, + }, + }, + }, + responses: { + "200": { + description: "The email template was successfully created", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/EmailTemplates", + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/src/schema/newsletterschema.ts b/src/schema/newsletterschema.ts index 5c43f8f1..04fc1882 100644 --- a/src/schema/newsletterschema.ts +++ b/src/schema/newsletterschema.ts @@ -1,73 +1,73 @@ -import { array, number, object, string, TypeOf } from "zod"; - -/** - * @openapi - * components: - * schemas: - * Newsletter: - * type: object - * properties: - * id: - * type: string - * example: "newsletterId123" - * title: - * type: string - * example: "Weekly Update" - * content: - * type: string - * example: "This is the content of the newsletter." - * PaginationMeta: - * type: object - * properties: - * total: - * type: integer - * example: 100 - * page: - * type: integer - * example: 1 - * limit: - * type: integer - * example: 10 - * totalPages: - * type: integer - * example: 10 - * GetAllNewslettersResponse: - * type: object - * properties: - * status: - * type: string - * example: "success" - * message: - * type: string - * example: "Newsletters retrieved successfully" - * data: - * type: array - * items: - * $ref: '#/components/schemas/Newsletter' - * meta: - * $ref: '#/components/schemas/PaginationMeta' - */ - -const newsletterSchema = object({ - id: string(), - title: string(), - content: string(), -}); - -const paginationMetaSchema = object({ - total: number(), - page: number(), - limit: number(), - totalPages: number(), -}); - -const getAllNewslettersResponseSchema = object({ - status: string().default("success"), - message: string().default("Newsletters retrieved successfully"), - data: array(newsletterSchema), - meta: paginationMetaSchema, -}); - -export type GetAllNewslettersResponse = TypeOf< - typeof getAllNewslettersResponseSchema ->; +import { array, number, object, string, TypeOf } from "zod"; + +/** + * @openapi + * components: + * schemas: + * Newsletter: + * type: object + * properties: + * id: + * type: string + * example: "newsletterId123" + * title: + * type: string + * example: "Weekly Update" + * content: + * type: string + * example: "This is the content of the newsletter." + * PaginationMeta: + * type: object + * properties: + * total: + * type: integer + * example: 100 + * page: + * type: integer + * example: 1 + * limit: + * type: integer + * example: 10 + * totalPages: + * type: integer + * example: 10 + * GetAllNewslettersResponse: + * type: object + * properties: + * status: + * type: string + * example: "success" + * message: + * type: string + * example: "Newsletters retrieved successfully" + * data: + * type: array + * items: + * $ref: '#/components/schemas/Newsletter' + * meta: + * $ref: '#/components/schemas/PaginationMeta' + */ + +const newsletterSchema = object({ + id: string(), + title: string(), + content: string(), +}); + +const paginationMetaSchema = object({ + total: number(), + page: number(), + limit: number(), + totalPages: number(), +}); + +const getAllNewslettersResponseSchema = object({ + status: string().default("success"), + message: string().default("Newsletters retrieved successfully"), + data: array(newsletterSchema), + meta: paginationMetaSchema, +}); + +export type GetAllNewslettersResponse = TypeOf< + typeof getAllNewslettersResponseSchema +>; diff --git a/src/schema/organization.schema.ts b/src/schema/organization.schema.ts index 0d8cbb42..0a7ac6e9 100644 --- a/src/schema/organization.schema.ts +++ b/src/schema/organization.schema.ts @@ -1,89 +1,89 @@ -import { array, object, string, TypeOf } from "zod"; - -/** - * @openapi - * components: - * schemas: - * OrganizationRole: - * type: object - * properties: - * id: - * type: string - * example: "roleId123" - * name: - * type: string - * example: "Admin" - * description: - * type: string - * example: "Administrator role with full access" - * GetAllOrganizationRolesResponse: - * type: object - * properties: - * status_code: - * type: integer - * example: 200 - * data: - * type: array - * items: - * $ref: '#/components/schemas/OrganizationRole' - */ - -const getAllOrganizationRolesResponseSchema = object({ - status_code: string().regex( - /^\d{3}$/, - "Status code should be a three-digit number", - ), - data: array( - object({ - id: string(), - name: string(), - description: string().optional(), - }), - ), -}); - -export type GetAllOrganizationRolesResponse = TypeOf< - typeof getAllOrganizationRolesResponseSchema ->; - -/** - * @openapi - * components: - * schemas: - * SingleRoleResponse: - * type: object - * properties: - * status_code: - * type: integer - * example: 200 - * data: - * type: object - * properties: - * id: - * type: string - * example: "roleId123" - * name: - * type: string - * example: "Admin" - * description: - * type: string - * example: "Administrator role with full access" - * message: - * type: string - * example: "The role with ID roleId123 does not exist in the organisation" - */ - -const singleRoleResponseSchema = object({ - status_code: string().regex( - /^\d{3}$/, - "Status code should be a three-digit number", - ), - data: object({ - id: string().optional(), - name: string().optional(), - description: string().optional(), - }).optional(), - message: string().optional(), -}); - -export type SingleRoleResponse = TypeOf; +import { array, object, string, TypeOf } from "zod"; + +/** + * @openapi + * components: + * schemas: + * OrganizationRole: + * type: object + * properties: + * id: + * type: string + * example: "roleId123" + * name: + * type: string + * example: "Admin" + * description: + * type: string + * example: "Administrator role with full access" + * GetAllOrganizationRolesResponse: + * type: object + * properties: + * status_code: + * type: integer + * example: 200 + * data: + * type: array + * items: + * $ref: '#/components/schemas/OrganizationRole' + */ + +const getAllOrganizationRolesResponseSchema = object({ + status_code: string().regex( + /^\d{3}$/, + "Status code should be a three-digit number", + ), + data: array( + object({ + id: string(), + name: string(), + description: string().optional(), + }), + ), +}); + +export type GetAllOrganizationRolesResponse = TypeOf< + typeof getAllOrganizationRolesResponseSchema +>; + +/** + * @openapi + * components: + * schemas: + * SingleRoleResponse: + * type: object + * properties: + * status_code: + * type: integer + * example: 200 + * data: + * type: object + * properties: + * id: + * type: string + * example: "roleId123" + * name: + * type: string + * example: "Admin" + * description: + * type: string + * example: "Administrator role with full access" + * message: + * type: string + * example: "The role with ID roleId123 does not exist in the organisation" + */ + +const singleRoleResponseSchema = object({ + status_code: string().regex( + /^\d{3}$/, + "Status code should be a three-digit number", + ), + data: object({ + id: string().optional(), + name: string().optional(), + description: string().optional(), + }).optional(), + message: string().optional(), +}); + +export type SingleRoleResponse = TypeOf; diff --git a/src/schema/product.schema.ts b/src/schema/product.schema.ts index f0cac3ec..164498ff 100644 --- a/src/schema/product.schema.ts +++ b/src/schema/product.schema.ts @@ -1,13 +1,13 @@ -import { z } from "zod"; - -export const productSchema = z.object({ - name: z.string().min(1, "Name is required"), - description: z.string().optional(), - category: z.string().optional(), - price: z.number().min(1, "Price is required"), - image: z.string().optional(), - quantity: z.number().min(1, "Quantity is required"), -}); - -export type ProductSchema = z.infer; -export default { productSchema }; +import { z } from "zod"; + +export const productSchema = z.object({ + name: z.string().min(1, "Name is required"), + description: z.string().optional(), + category: z.string().optional(), + price: z.number().min(1, "Price is required"), + image: z.string().optional(), + quantity: z.number().min(1, "Quantity is required"), +}); + +export type ProductSchema = z.infer; +export default { productSchema }; diff --git a/src/schema/squeezeSchema.ts b/src/schema/squeezeSchema.ts index 250f4683..d423b680 100644 --- a/src/schema/squeezeSchema.ts +++ b/src/schema/squeezeSchema.ts @@ -1,68 +1,68 @@ -import { z } from "zod"; - -/** - * @openapi - * components: - * schemas: - * Squeeze: - * type: object - * properties: - * id: - * type: string - * format: uuid - * example: "e02c7c4e-bb92-4f9a-8d91-bc9e63c5b8d5" - * email: - * type: string - * format: email - * example: "naina@example.com" - * first_name: - * type: string - * example: "Nainah" - * last_name: - * type: string - * example: "Kamah" - * phone: - * type: string - * example: "+254123456789" - * location: - * type: string - * example: "Nairobi" - * job_title: - * type: string - * example: "Backend Engineer" - * company: - * type: string - * example: "HNG" - * interests: - * type: array - * example: ["Tech", "Politics", "Youth"] - * referral_source: - * type: string - * example: "Internet" - * createdAt: - * type: string - * format: date-time - * example: "2024-08-03T14:00:00Z" - * updatedAt: - * type: string - * format: date-time - * example: "2024-08-03T14:00:00Z" - * required: - * - id - * - email - * - createdAt - * - updatedAt - */ -const squeezeSchema = z.object({ - email: z.string().email("Invalid email address"), - first_name: z.string().min(1, "First name is required"), - last_name: z.string().min(1, "Last name is required"), - phone: z.string().optional(), - location: z.string().optional(), - job_title: z.string().optional(), - company: z.string().optional(), - interests: z.array(z.string()).optional(), - referral_source: z.string().optional(), -}); - -export { squeezeSchema }; +import { z } from "zod"; + +/** + * @openapi + * components: + * schemas: + * Squeeze: + * type: object + * properties: + * id: + * type: string + * format: uuid + * example: "e02c7c4e-bb92-4f9a-8d91-bc9e63c5b8d5" + * email: + * type: string + * format: email + * example: "naina@example.com" + * first_name: + * type: string + * example: "Nainah" + * last_name: + * type: string + * example: "Kamah" + * phone: + * type: string + * example: "+254123456789" + * location: + * type: string + * example: "Nairobi" + * job_title: + * type: string + * example: "Backend Engineer" + * company: + * type: string + * example: "HNG" + * interests: + * type: array + * example: ["Tech", "Politics", "Youth"] + * referral_source: + * type: string + * example: "Internet" + * createdAt: + * type: string + * format: date-time + * example: "2024-08-03T14:00:00Z" + * updatedAt: + * type: string + * format: date-time + * example: "2024-08-03T14:00:00Z" + * required: + * - id + * - email + * - createdAt + * - updatedAt + */ +const squeezeSchema = z.object({ + email: z.string().email("Invalid email address"), + first_name: z.string().min(1, "First name is required"), + last_name: z.string().min(1, "Last name is required"), + phone: z.string().optional(), + location: z.string().optional(), + job_title: z.string().optional(), + company: z.string().optional(), + interests: z.array(z.string()).optional(), + referral_source: z.string().optional(), +}); + +export { squeezeSchema }; diff --git a/src/schema/user.schema.ts b/src/schema/user.schema.ts index f10dd4b9..30c27832 100644 --- a/src/schema/user.schema.ts +++ b/src/schema/user.schema.ts @@ -1,173 +1,173 @@ -import { array, number, object, string, TypeOf } from "zod"; - -/** - * @openapi - * components: - * schemas: - * User: - * type: object - * properties: - * user_name: - * type: string - * example: "Lewis" - * email: - * type: string - * example: "lewis@gmail.com" - * UserProfile: - * type: object - * properties: - * id: - * type: string - * example: "58b6" - * user_name: - * type: string - * example: "yasuke" - * email: - * type: string - * example: "sam@gmail.com" - * profile_picture: - * type: string - * example: "https://avatar.com" - * bio: - * type: string - * example: "Developer at HNG" - * GetProfileResponse: - * type: object - * properties: - * status_code: - * type: number - * example: 200 - * data: - * $ref: '#/components/schemas/UserProfile' - * GetAllUsersResponse: - * type: object - * properties: - * status: - * type: string - * example: "success" - * status_code: - * type: number - * example: 200 - * message: - * type: string - * example: "Users retrieved successfully" - * pagination: - * type: object - * properties: - * totalItems: - * type: number - * example: 100 - * totalPages: - * type: number - * example: 10 - * currentPage: - * type: number - * example: 1 - * data: - * type: array - * items: - * $ref: '#/components/schemas/User' - * DeleteUserResponse: - * type: object - * properties: - * status: - * type: string - * example: "success" - * status_code: - * type: number - * example: 202 - * message: - * type: string - * example: "User deleted successfully" - */ - -/** - * @openapi - * components: - * schemas: - * Timezone: - * type: object - * properties: - * timezone: - * type: string - * example: "America/New_York" - * gmtOffset: - * type: string - * example: "-05:00" - * description: - * type: string - * example: "Eastern Standard Time" - */ - -const payload = object({ - id: string(), - user_name: string(), - email: string(), - profile_picture: string(), - bio: string(), -}); - -const paginationSchema = object({ - totalItems: number(), - totalPages: number(), - currentPage: number(), -}); - -const params = { - params: object({ - id: string({ - required_error: "userId is required", - }), - }), -}; - -export const getUserProfileSchema = object({ - response: object({ - status_code: number(), - data: payload, - }), -}); - -export const getAllUsersSchema = object({ - response: object({ - status: string(), - status_code: number(), - message: string(), - pagination: paginationSchema, - data: array(payload), - }), -}); - -export const deleteUserSchema = object({ - ...params, - response: object({ - status: string(), - status_code: number(), - message: string(), - }), -}); - -const timezoneSchema = object({ - timezone: string({ - required_error: "Timezone is required", - }), - gmtOffset: string({ - required_error: "GMT Offset is required", - }), - description: string({ - required_error: "Description is required", - }), -}); - -export const updateUserTimezoneSchema = object({ - ...params, - body: timezoneSchema, -}); - -export type GetUserProfileResponse = TypeOf< - typeof getUserProfileSchema ->["response"]; -export type GetAllUsersResponse = TypeOf["response"]; -export type DeleteUserInput = TypeOf; -export type DeleteUserResponse = TypeOf["response"]; -export type UpdateUserTimezoneInput = TypeOf; +import { array, number, object, string, TypeOf } from "zod"; + +/** + * @openapi + * components: + * schemas: + * User: + * type: object + * properties: + * user_name: + * type: string + * example: "Lewis" + * email: + * type: string + * example: "lewis@gmail.com" + * UserProfile: + * type: object + * properties: + * id: + * type: string + * example: "58b6" + * user_name: + * type: string + * example: "yasuke" + * email: + * type: string + * example: "sam@gmail.com" + * profile_picture: + * type: string + * example: "https://avatar.com" + * bio: + * type: string + * example: "Developer at HNG" + * GetProfileResponse: + * type: object + * properties: + * status_code: + * type: number + * example: 200 + * data: + * $ref: '#/components/schemas/UserProfile' + * GetAllUsersResponse: + * type: object + * properties: + * status: + * type: string + * example: "success" + * status_code: + * type: number + * example: 200 + * message: + * type: string + * example: "Users retrieved successfully" + * pagination: + * type: object + * properties: + * totalItems: + * type: number + * example: 100 + * totalPages: + * type: number + * example: 10 + * currentPage: + * type: number + * example: 1 + * data: + * type: array + * items: + * $ref: '#/components/schemas/User' + * DeleteUserResponse: + * type: object + * properties: + * status: + * type: string + * example: "success" + * status_code: + * type: number + * example: 202 + * message: + * type: string + * example: "User deleted successfully" + */ + +/** + * @openapi + * components: + * schemas: + * Timezone: + * type: object + * properties: + * timezone: + * type: string + * example: "America/New_York" + * gmtOffset: + * type: string + * example: "-05:00" + * description: + * type: string + * example: "Eastern Standard Time" + */ + +const payload = object({ + id: string(), + user_name: string(), + email: string(), + profile_picture: string(), + bio: string(), +}); + +const paginationSchema = object({ + totalItems: number(), + totalPages: number(), + currentPage: number(), +}); + +const params = { + params: object({ + id: string({ + required_error: "userId is required", + }), + }), +}; + +export const getUserProfileSchema = object({ + response: object({ + status_code: number(), + data: payload, + }), +}); + +export const getAllUsersSchema = object({ + response: object({ + status: string(), + status_code: number(), + message: string(), + pagination: paginationSchema, + data: array(payload), + }), +}); + +export const deleteUserSchema = object({ + ...params, + response: object({ + status: string(), + status_code: number(), + message: string(), + }), +}); + +const timezoneSchema = object({ + timezone: string({ + required_error: "Timezone is required", + }), + gmtOffset: string({ + required_error: "GMT Offset is required", + }), + description: string({ + required_error: "Description is required", + }), +}); + +export const updateUserTimezoneSchema = object({ + ...params, + body: timezoneSchema, +}); + +export type GetUserProfileResponse = TypeOf< + typeof getUserProfileSchema +>["response"]; +export type GetAllUsersResponse = TypeOf["response"]; +export type DeleteUserInput = TypeOf; +export type DeleteUserResponse = TypeOf["response"]; +export type UpdateUserTimezoneInput = TypeOf; diff --git a/src/seeder.ts b/src/seeder.ts index c9bc0e21..039b2d82 100644 --- a/src/seeder.ts +++ b/src/seeder.ts @@ -1,125 +1,125 @@ -// // // src/seeder.ts -// // import AppDataSource from "./data-source"; -// // import { User, Organization, Product, Profile } from "./models"; -// // import log from "./utils/logger"; - -// const createUsers = async () => { -// try { -// log.info("Creating user1..."); -// const user1 = new User(); -// user1.name = "John Doe"; -// user1.email = "johndoe@example.com"; -// user1.password = "password"; -// user1.otp = Math.floor(Math.random() * 10000); -// user1.otp_expires_at = new Date(Date.now() + 3600 * 1000); -// user1.profile = new Profile(); -// user1.profile.first_name = "John"; -// user1.profile.last_name = "Doe"; -// user1.profile.phone = "1234567890"; -// user1.profile.avatarUrl = "http://example.com/avatar.jpg"; - -// log.info("User1 created: ", user1); - -// log.info("Creating user2..."); -// const user2 = new User(); -// user2.name = "Jane Doe"; -// user2.email = "janedoe@example.com"; -// user2.password = "password"; -// user2.otp = Math.floor(Math.random() * 10000); -// user2.otp_expires_at = new Date(Date.now() + 3600 * 1000); -// user2.profile = new Profile(); -// user2.profile.first_name = "Jane"; -// user2.profile.last_name = "Doe"; -// user2.profile.phone = "0987654321"; -// user2.profile.avatarUrl = "http://example.com/avatar.jpg"; - -// log.info("User2 created: ", user2); - -// log.info("Saving users..."); -// await AppDataSource.manager.save([user1, user2]); -// log.info("Users created successfully"); -// return [user1, user2]; -// } catch (error) { -// log.error("Error creating users: ", error.message); -// log.error(error.stack); -// throw error; -// } -// }; - -// const createProducts = async (users: User[]) => { -// try { -// log.info("Creating products..."); -// const product1 = new Product(); -// product1.name = "Product 1"; -// product1.description = "Description for product 1"; -// product1.price = 1099; -// product1.category = "Category 1"; -// product1.user = users[0]; - -// const product2 = new Product(); -// product2.name = "Product 2"; -// product2.description = "Description for product 2"; -// product2.price = 1999; -// product2.category = "Category 2"; -// product2.user = users[0]; - -// const product3 = new Product(); -// product3.name = "Product 3"; -// product3.description = "Description for product 3"; -// product3.price = 2999; -// product3.category = "Category 3"; -// product3.user = users[1]; - -// const product4 = new Product(); -// product4.name = "Product 4"; -// product4.description = "Description for product 4"; -// product4.price = 3999; -// product4.category = "Category 4"; -// product4.user = users[1]; - -// log.info("Saving products..."); -// await AppDataSource.manager.save([product1, product2, product3, product4]); -// log.info("Products created successfully"); -// } catch (error) { -// log.error("Error creating products: ", error.message); -// log.error(error.stack); -// throw error; -// } -// }; - -// const createOrganizations = async (users: User[]) => { -// try { -// log.info("Creating organizations..."); -// const organization1 = new Organization(); -// organization1.name = "Org 1"; -// organization1.owner_id = users[0].id; // Set owner_id -// organization1.description = "Description for org 1"; - -// const organization2 = new Organization(); -// organization2.name = "Org 2"; -// organization2.owner_id = users[0].id; // Set owner_id -// organization2.description = "Description for org 2"; - -// // const organization3 = new Organization(); -// // organization3.name = "Org 3"; -// // organization3.description = "Description for org 3"; -// // organization3.owner_id = user2.id; - -// // // Assign organizations to users -// // user1.organizations = [organization1, organization2]; -// // user2.organizations = [organization1, organization2, organization3]; - -// // // Save entities - -// // await AppDataSource.manager.save(organization1); -// // await AppDataSource.manager.save(organization2); -// // await AppDataSource.manager.save(organization3); -// // await AppDataSource.manager.save(product1); -// // await AppDataSource.manager.save(product2); -// // await AppDataSource.manager.save(product3); -// // await AppDataSource.manager.save(product4); - -// // log.info("Seeding completed successfully."); -// // }; - -// // export { seed }; +// // // src/seeder.ts +// // import AppDataSource from "./data-source"; +// // import { User, Organization, Product, Profile } from "./models"; +// // import log from "./utils/logger"; + +// const createUsers = async () => { +// try { +// log.info("Creating user1..."); +// const user1 = new User(); +// user1.name = "John Doe"; +// user1.email = "johndoe@example.com"; +// user1.password = "password"; +// user1.otp = Math.floor(Math.random() * 10000); +// user1.otp_expires_at = new Date(Date.now() + 3600 * 1000); +// user1.profile = new Profile(); +// user1.profile.first_name = "John"; +// user1.profile.last_name = "Doe"; +// user1.profile.phone = "1234567890"; +// user1.profile.avatarUrl = "http://example.com/avatar.jpg"; + +// log.info("User1 created: ", user1); + +// log.info("Creating user2..."); +// const user2 = new User(); +// user2.name = "Jane Doe"; +// user2.email = "janedoe@example.com"; +// user2.password = "password"; +// user2.otp = Math.floor(Math.random() * 10000); +// user2.otp_expires_at = new Date(Date.now() + 3600 * 1000); +// user2.profile = new Profile(); +// user2.profile.first_name = "Jane"; +// user2.profile.last_name = "Doe"; +// user2.profile.phone = "0987654321"; +// user2.profile.avatarUrl = "http://example.com/avatar.jpg"; + +// log.info("User2 created: ", user2); + +// log.info("Saving users..."); +// await AppDataSource.manager.save([user1, user2]); +// log.info("Users created successfully"); +// return [user1, user2]; +// } catch (error) { +// log.error("Error creating users: ", error.message); +// log.error(error.stack); +// throw error; +// } +// }; + +// const createProducts = async (users: User[]) => { +// try { +// log.info("Creating products..."); +// const product1 = new Product(); +// product1.name = "Product 1"; +// product1.description = "Description for product 1"; +// product1.price = 1099; +// product1.category = "Category 1"; +// product1.user = users[0]; + +// const product2 = new Product(); +// product2.name = "Product 2"; +// product2.description = "Description for product 2"; +// product2.price = 1999; +// product2.category = "Category 2"; +// product2.user = users[0]; + +// const product3 = new Product(); +// product3.name = "Product 3"; +// product3.description = "Description for product 3"; +// product3.price = 2999; +// product3.category = "Category 3"; +// product3.user = users[1]; + +// const product4 = new Product(); +// product4.name = "Product 4"; +// product4.description = "Description for product 4"; +// product4.price = 3999; +// product4.category = "Category 4"; +// product4.user = users[1]; + +// log.info("Saving products..."); +// await AppDataSource.manager.save([product1, product2, product3, product4]); +// log.info("Products created successfully"); +// } catch (error) { +// log.error("Error creating products: ", error.message); +// log.error(error.stack); +// throw error; +// } +// }; + +// const createOrganizations = async (users: User[]) => { +// try { +// log.info("Creating organizations..."); +// const organization1 = new Organization(); +// organization1.name = "Org 1"; +// organization1.owner_id = users[0].id; // Set owner_id +// organization1.description = "Description for org 1"; + +// const organization2 = new Organization(); +// organization2.name = "Org 2"; +// organization2.owner_id = users[0].id; // Set owner_id +// organization2.description = "Description for org 2"; + +// // const organization3 = new Organization(); +// // organization3.name = "Org 3"; +// // organization3.description = "Description for org 3"; +// // organization3.owner_id = user2.id; + +// // // Assign organizations to users +// // user1.organizations = [organization1, organization2]; +// // user2.organizations = [organization1, organization2, organization3]; + +// // // Save entities + +// // await AppDataSource.manager.save(organization1); +// // await AppDataSource.manager.save(organization2); +// // await AppDataSource.manager.save(organization3); +// // await AppDataSource.manager.save(product1); +// // await AppDataSource.manager.save(product2); +// // await AppDataSource.manager.save(product3); +// // await AppDataSource.manager.save(product4); + +// // log.info("Seeding completed successfully."); +// // }; + +// // export { seed }; diff --git a/src/services/admin.services.ts b/src/services/admin.services.ts index a4825a1e..2ca57248 100644 --- a/src/services/admin.services.ts +++ b/src/services/admin.services.ts @@ -1,205 +1,205 @@ -// / src/services/AdminOrganisationService.ts -import { NextFunction, Request, Response } from "express"; -// import { getRepository, Repository } from 'typeorm'; -import { User, Organization, Log } from "../models"; -import AppDataSource from "../data-source"; -import { HttpError } from "../middleware"; -import { hashPassword } from "../utils/index"; - -export class AdminOrganisationService { - public async update(req: Request): Promise { - try { - const { - name, - email, - industry, - type, - country, - address, - state, - description, - } = req.body; - const org_id = req.params.id; - - const orgRepository = AppDataSource.getRepository(Organization); - // Check if organisation exists - const oldOrg = await orgRepository.findOne({ - where: { id: org_id }, - }); - - if (!oldOrg) { - throw new HttpError( - 404, - "Organisation not found, please check and try again", - ); - } - - //Update Organisation on DB - await orgRepository.update(org_id, { - name, - email, - industry, - type, - country, - address, - state, - description, - }); - //Fetch Updated organisation - const newOrg = await orgRepository.findOne({ - where: { id: org_id }, - }); - return newOrg; - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - } - - public async deleteOrganization(orgId: string): Promise { - const organizationRepository = AppDataSource.getRepository(Organization); - const organization = await organizationRepository.findOne({ - where: { id: orgId }, - }); - - if (!organization) { - throw new HttpError(404, "Organization not found"); - } - - try { - await organizationRepository.remove(organization); - } catch (error) { - throw new HttpError(500, "Deletion failed"); - } - - return organization; // Return the deleted organization - } - - public async setUserRole(req: Request): Promise { - try { - const { role } = req.body; - const { user_id } = req.params; - - const userRepository = AppDataSource.getRepository(User); - - const user = await userRepository.findOne({ - where: { id: user_id }, - }); - - if (!user) { - throw new HttpError(404, "User not Found"); - } - - // Update User Role on the Database - user.role = role; - await userRepository.save(user); - - return user; - } catch (error) { - throw new HttpError(error.status_code || 500, error.message); - } - } -} - -export class AdminUserService { - async getPaginatedUsers( - page: number, - limit: number, - ): Promise<{ users: User[]; totalUsers: number }> { - const userRepository = AppDataSource.getRepository(User); - - const [users, totalUsers] = await userRepository.findAndCount({ - skip: (page - 1) * limit, - take: limit, - }); - - return { users, totalUsers }; - } - public async updateUser(req: Request): Promise { - try { - const { firstName, lastName, email, role, password, isverified } = - req.body; - - const userRepository = AppDataSource.getRepository(User); - - const existingUser = await userRepository.findOne({ - where: { email }, - }); - if (!existingUser) { - throw new HttpError(404, "User not found"); - } - - let hashedPassword: string | undefined; - if (password) { - hashedPassword = await hashPassword(password); - } - - const updatedFields = { - name: `${firstName} ${lastName}`, - email, - role, - password: hashedPassword || existingUser.password, - isverified: - isverified !== undefined ? isverified : existingUser.isverified, - }; - - await userRepository.update(existingUser.id, updatedFields); - - const updatedUser = await userRepository.findOne({ - where: { id: existingUser.id }, - }); - return updatedUser!; - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - } - - public async getSingleUser(userId: string): Promise { - try { - const userRepository = AppDataSource.getRepository(User); - const user = await userRepository.findOne({ - where: { id: userId }, - }); - if (!user) { - throw new HttpError(404, "User not found"); - } - return user; - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - } -} - -export class AdminLogService { - public async getPaginatedLogs(req: Request): Promise<{ - logs: Log[]; - totalLogs: number; - totalPages: number; - currentPage: number; - }> { - try { - const { page = 1, limit = 10, sort = "desc", offset = 0 } = req.query; - const logRepository = AppDataSource.getRepository(Log); - - const [logs, totalLogs] = await logRepository.findAndCount({ - order: { id: sort === "asc" ? "ASC" : "DESC" }, - skip: Number(offset), - take: Number(limit), - }); - - const totalPages = Math.ceil(totalLogs / Number(limit)); - - if (!logs.length) { - throw new HttpError(404, "Logs not found"); - } - - return { - logs, - totalLogs, - totalPages, - currentPage: Number(page), - }; - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - } -} +// / src/services/AdminOrganisationService.ts +import { NextFunction, Request, Response } from "express"; +// import { getRepository, Repository } from 'typeorm'; +import { User, Organization, Log } from "../models"; +import AppDataSource from "../data-source"; +import { HttpError } from "../middleware"; +import { hashPassword } from "../utils/index"; + +export class AdminOrganisationService { + public async update(req: Request): Promise { + try { + const { + name, + email, + industry, + type, + country, + address, + state, + description, + } = req.body; + const org_id = req.params.id; + + const orgRepository = AppDataSource.getRepository(Organization); + // Check if organisation exists + const oldOrg = await orgRepository.findOne({ + where: { id: org_id }, + }); + + if (!oldOrg) { + throw new HttpError( + 404, + "Organisation not found, please check and try again", + ); + } + + //Update Organisation on DB + await orgRepository.update(org_id, { + name, + email, + industry, + type, + country, + address, + state, + description, + }); + //Fetch Updated organisation + const newOrg = await orgRepository.findOne({ + where: { id: org_id }, + }); + return newOrg; + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + } + + public async deleteOrganization(orgId: string): Promise { + const organizationRepository = AppDataSource.getRepository(Organization); + const organization = await organizationRepository.findOne({ + where: { id: orgId }, + }); + + if (!organization) { + throw new HttpError(404, "Organization not found"); + } + + try { + await organizationRepository.remove(organization); + } catch (error) { + throw new HttpError(500, "Deletion failed"); + } + + return organization; // Return the deleted organization + } + + public async setUserRole(req: Request): Promise { + try { + const { role } = req.body; + const { user_id } = req.params; + + const userRepository = AppDataSource.getRepository(User); + + const user = await userRepository.findOne({ + where: { id: user_id }, + }); + + if (!user) { + throw new HttpError(404, "User not Found"); + } + + // Update User Role on the Database + user.role = role; + await userRepository.save(user); + + return user; + } catch (error) { + throw new HttpError(error.status_code || 500, error.message); + } + } +} + +export class AdminUserService { + async getPaginatedUsers( + page: number, + limit: number, + ): Promise<{ users: User[]; totalUsers: number }> { + const userRepository = AppDataSource.getRepository(User); + + const [users, totalUsers] = await userRepository.findAndCount({ + skip: (page - 1) * limit, + take: limit, + }); + + return { users, totalUsers }; + } + public async updateUser(req: Request): Promise { + try { + const { firstName, lastName, email, role, password, isverified } = + req.body; + + const userRepository = AppDataSource.getRepository(User); + + const existingUser = await userRepository.findOne({ + where: { email }, + }); + if (!existingUser) { + throw new HttpError(404, "User not found"); + } + + let hashedPassword: string | undefined; + if (password) { + hashedPassword = await hashPassword(password); + } + + const updatedFields = { + name: `${firstName} ${lastName}`, + email, + role, + password: hashedPassword || existingUser.password, + isverified: + isverified !== undefined ? isverified : existingUser.isverified, + }; + + await userRepository.update(existingUser.id, updatedFields); + + const updatedUser = await userRepository.findOne({ + where: { id: existingUser.id }, + }); + return updatedUser!; + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + } + + public async getSingleUser(userId: string): Promise { + try { + const userRepository = AppDataSource.getRepository(User); + const user = await userRepository.findOne({ + where: { id: userId }, + }); + if (!user) { + throw new HttpError(404, "User not found"); + } + return user; + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + } +} + +export class AdminLogService { + public async getPaginatedLogs(req: Request): Promise<{ + logs: Log[]; + totalLogs: number; + totalPages: number; + currentPage: number; + }> { + try { + const { page = 1, limit = 10, sort = "desc", offset = 0 } = req.query; + const logRepository = AppDataSource.getRepository(Log); + + const [logs, totalLogs] = await logRepository.findAndCount({ + order: { id: sort === "asc" ? "ASC" : "DESC" }, + skip: Number(offset), + take: Number(limit), + }); + + const totalPages = Math.ceil(totalLogs / Number(limit)); + + if (!logs.length) { + throw new HttpError(404, "Logs not found"); + } + + return { + logs, + totalLogs, + totalPages, + currentPage: Number(page), + }; + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + } +} diff --git a/src/services/auth.services.ts b/src/services/auth.services.ts index 639aac6e..6edd4607 100644 --- a/src/services/auth.services.ts +++ b/src/services/auth.services.ts @@ -1,407 +1,407 @@ -// auth service -import bcrypt from "bcryptjs"; -import crypto from "crypto"; -import jwt from "jsonwebtoken"; -import { MoreThan } from "typeorm"; -import config from "../config"; -import APP_CONFIG from "../config/app.config"; -import AppDataSource from "../data-source"; -import { - BadRequest, - Conflict, - HttpError, - ResourceNotFound, - ServerError, -} from "../middleware"; -import { Profile, User } from "../models"; -import { IAuthService, IUserLogin, IUserSignUp } from "../types"; -import { - comparePassword, - generateAccessToken, - generateNumericOTP, - generateToken, - hashPassword, - verifyToken, -} from "../utils"; -import { Sendmail } from "../utils/mail"; -import { addEmailToQueue } from "../utils/queue"; -import renderTemplate from "../views/email/renderTemplate"; -import { generateMagicLinkEmail } from "../views/magic-link.email"; -import { compilerOtp } from "../views/welcome"; -import speakeasy from "speakeasy"; -import { UserService } from "./user.services"; - -export class AuthService implements IAuthService { - private userService: UserService; - constructor() { - this.userService = new UserService(); - } - public async signUp(payload: IUserSignUp): Promise<{ - message: string; - user: Partial; - access_token: string; - }> { - const { first_name, last_name, email, password } = payload; - - try { - const userExists = await User.findOne({ - where: { email }, - }); - - if (userExists) { - throw new Conflict("User already exists"); - } - const hashedPassword = await hashPassword(password); - const otp = generateNumericOTP(6); - const otpExpires = new Date(Date.now() + 10 * 60 * 1000); - const user = new User(); - user.name = `${first_name} ${last_name}`; - user.email = email; - user.password = hashedPassword; - user.profile = new Profile(); - user.profile.first_name = first_name; - user.profile.last_name = last_name; - user.profile.avatarUrl = ""; - user.otp = parseInt(otp); - user.otp_expires_at = otpExpires; - user.isverified = true; - - const createdUser = await AppDataSource.manager.save(user); - const access_token = jwt.sign( - { userId: createdUser.id }, - config.TOKEN_SECRET, - { - expiresIn: "1d", - }, - ); - - const { - password: _, - otp: __, - otp_expires_at: ___, - ...rest - } = createdUser; - - return { user: rest, access_token, message: "user created" }; - } catch (error) { - if (error instanceof HttpError) { - throw error; - } - throw new HttpError(error.status || 500, error.message || error); - } - } - - public async verifyEmail( - token: string, - otp: number, - ): Promise<{ message: string }> { - try { - const decoded: any = jwt.verify(token, config.TOKEN_SECRET); - const userId = decoded.userId; - - const user = await User.findOne({ where: { id: userId } }); - if (!user) { - throw new HttpError(404, "User not found"); - } - - if (user.otp !== otp || user.otp_expires_at < new Date()) { - throw new HttpError(400, "Invalid OTP "); - } - - user.isverified = true; - await AppDataSource.manager.save(user); - - return { message: "Email successfully verified" }; - } catch (error) { - if (error.name === "TokenExpiredError") { - throw new HttpError(400, "Verification token has expired"); - } - throw new HttpError(error.status || 500, error.message || error); - } - } - - public async login( - payload: IUserLogin, - ): Promise<{ access_token: string; user: Partial }> { - const { email, password } = payload; - - try { - const user = await User.findOne({ - where: { email }, - relations: ["profile"], - }); - - if (!user) { - throw new HttpError(404, "User not found"); - } - - if (user.google_id && user.password === null) { - throw new HttpError(401, "User Created with Google"); - } - - const isPasswordValid = await comparePassword(password, user.password); - if (!isPasswordValid) { - throw new HttpError(401, "Invalid credentials"); - } - - if (!user.isverified) { - throw new HttpError(403, "Email not verified"); - } - - const access_token = jwt.sign({ userId: user.id }, config.TOKEN_SECRET, { - expiresIn: "1d", - }); - - const { password: _, ...userWithoutPassword } = user; - - return { access_token, user: userWithoutPassword }; - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - } - - resetPassword = async ( - token: string, - newPassword: string, - ): Promise<{ message: string }> => { - try { - const hashedToken = crypto - .createHash("sha256") - .update(token) - .digest("hex"); - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(newPassword, salt); - - const user = await User.findOne({ - where: { - passwordResetToken: hashedToken, - passwordResetExpires: MoreThan(Date.now()), - }, - }); - - if (!user) { - throw new HttpError(404, "Token is invalid or has expired"); - } - - user.password = hashedPassword; - user.passwordResetToken = undefined; - user.passwordResetExpires = undefined; - - await AppDataSource.manager.save(user); - - return { message: "Password reset successfully." }; - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - }; - - public async forgotPassword( - email: string, - resetURL: string, - ): Promise<{ message: string }> { - try { - const user = await User.findOne({ where: { email } }); - - if (!user) { - throw new HttpError(404, "User not found"); - } - - const resetToken = user.createPasswordResetToken(); - await AppDataSource.manager.save(user); - - const htmlTemplate = renderTemplate("password-reset", { - title: "Reset Password", - userName: user.name, - resetUrl: resetURL + `${resetToken}`, - }); - - const emailContent = { - from: `Boilerplate <${config.SMTP_USER}>`, - to: email, - subject: "Password Reset", - html: htmlTemplate, - }; - - await addEmailToQueue(emailContent); - - return { message: "Password reset link sent successfully." }; - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - } - - public async changePassword( - userId: string, - oldPassword: string, - newPassword: string, - confirmPassword: string, - ): Promise<{ message: string }> { - try { - const user = await User.findOne({ where: { id: userId } }); - - if (!user) { - throw new HttpError(404, "User not found"); - } - - const isOldPasswordValid = await comparePassword( - oldPassword, - user.password, - ); - if (!isOldPasswordValid) { - throw new HttpError(401, "Old password is incorrect"); - } - - if (oldPassword === newPassword) { - throw new HttpError( - 400, - "You used this password recently. Please choose a different one.", - ); - } - - if (newPassword !== confirmPassword) { - throw new HttpError(400, "New password and confirmation do not match"); - } - - user.password = await hashPassword(newPassword); - await AppDataSource.manager.save(user); - - return { message: "Password changed successfully" }; - } catch (error) { - throw error; - } - } - - public async generateMagicLink(email: string) { - try { - const user = await User.findOne({ where: { email } }); - if (user === null || !user) { - throw new ResourceNotFound("User is not registered"); - } - - const token = generateToken({ email: email }); - const protocol = APP_CONFIG.USE_HTTPS ? "https" : "http"; - const magicLinkUrl = `${protocol}://${config.BASE_URL}/api/v1/auth/magic-link?token=${token}`; - - const mailToBeSentToUser = await Sendmail({ - from: `Boilerplate `, - to: email, - subject: "MAGIC LINK LOGIN", - html: generateMagicLinkEmail(magicLinkUrl, email), - }); - - return { - ok: mailToBeSentToUser === "Email sent successfully.", - message: "Email sent successfully.", - user, - }; - } catch (err) { - throw err; - } - } - - public async validateMagicLinkToken(token: string) { - try { - const { email } = verifyToken(token as string); - if (!email) { - throw new BadRequest("Invalid JWT"); - } - - const user = await User.findOne({ - where: { email: String(email) }, - }); - - if (user === null || !user) { - throw new ResourceNotFound("User not found"); - } - - return { - status: "ok", - email: user.email, - userId: user.id, - }; - } catch (error) { - throw error; - } - } - - public async passwordlessLogin(userId: string) { - try { - const access_token = await generateAccessToken(userId); - - return { - access_token, - }; - } catch (error) { - throw error; - } - } - - public generate2FARecoveryCode() { - const codes = []; - for (let i = 0; i < 8; i++) { - codes.push(Math.random().toString(36).substring(2, 8).toUpperCase()); - } - return codes; - } - - public async enable2FA(user_id: string, password: string) { - try { - const user = await this.userService.getUserById(user_id); - - const is_password_valid = await this.userService.compareUserPassword( - password, - user.password, - ); - if (!is_password_valid) { - throw new BadRequest("Invalid password"); - } - if (user.is_2fa_enabled) { - throw new BadRequest("2FA is already enabled"); - } - - const secret = speakeasy.generateSecret({ length: 32 }); - const backup_codes = this.generate2FARecoveryCode(); - const payload = { - secret: secret.base32, - is_2fa_enabled: true, - backup_codes: backup_codes, - }; - - await this.userService.updateUserRecord({ - updatePayload: payload, - identifierOption: { - identifier: user_id, - identifierType: "id", - }, - }); - - const qrCodeUrl = speakeasy.otpauthURL({ - secret: secret.ascii, - label: `Hng:${user.email}`, - issuer: `Hng Boilerplate`, - }); - - return { - message: "2FA setup initiated", - data: { - secret: secret.base32, - qr_code_url: qrCodeUrl, - backup_codes, - }, - }; - } catch (error) { - if (error instanceof BadRequest) { - throw error; - } - throw new ServerError("An error occurred while trying to enable 2FA"); - } - } - - public verify2FA(totp_code: string, user: User) { - return speakeasy.totp.verify({ - secret: user.secret, - encoding: "base32", - token: totp_code, - }); - } -} +// auth service +import bcrypt from "bcryptjs"; +import crypto from "crypto"; +import jwt from "jsonwebtoken"; +import { MoreThan } from "typeorm"; +import config from "../config"; +import APP_CONFIG from "../config/app.config"; +import AppDataSource from "../data-source"; +import { + BadRequest, + Conflict, + HttpError, + ResourceNotFound, + ServerError, +} from "../middleware"; +import { Profile, User } from "../models"; +import { IAuthService, IUserLogin, IUserSignUp } from "../types"; +import { + comparePassword, + generateAccessToken, + generateNumericOTP, + generateToken, + hashPassword, + verifyToken, +} from "../utils"; +import { Sendmail } from "../utils/mail"; +import { addEmailToQueue } from "../utils/queue"; +import renderTemplate from "../views/email/renderTemplate"; +import { generateMagicLinkEmail } from "../views/magic-link.email"; +import { compilerOtp } from "../views/welcome"; +import speakeasy from "speakeasy"; +import { UserService } from "./user.services"; + +export class AuthService implements IAuthService { + private userService: UserService; + constructor() { + this.userService = new UserService(); + } + public async signUp(payload: IUserSignUp): Promise<{ + message: string; + user: Partial; + access_token: string; + }> { + const { first_name, last_name, email, password } = payload; + + try { + const userExists = await User.findOne({ + where: { email }, + }); + + if (userExists) { + throw new Conflict("User already exists"); + } + const hashedPassword = await hashPassword(password); + const otp = generateNumericOTP(6); + const otpExpires = new Date(Date.now() + 10 * 60 * 1000); + const user = new User(); + user.name = `${first_name} ${last_name}`; + user.email = email; + user.password = hashedPassword; + user.profile = new Profile(); + user.profile.first_name = first_name; + user.profile.last_name = last_name; + user.profile.avatarUrl = ""; + user.otp = parseInt(otp); + user.otp_expires_at = otpExpires; + user.isverified = true; + + const createdUser = await AppDataSource.manager.save(user); + const access_token = jwt.sign( + { userId: createdUser.id }, + config.TOKEN_SECRET, + { + expiresIn: "1d", + }, + ); + + const { + password: _, + otp: __, + otp_expires_at: ___, + ...rest + } = createdUser; + + return { user: rest, access_token, message: "user created" }; + } catch (error) { + if (error instanceof HttpError) { + throw error; + } + throw new HttpError(error.status || 500, error.message || error); + } + } + + public async verifyEmail( + token: string, + otp: number, + ): Promise<{ message: string }> { + try { + const decoded: any = jwt.verify(token, config.TOKEN_SECRET); + const userId = decoded.userId; + + const user = await User.findOne({ where: { id: userId } }); + if (!user) { + throw new HttpError(404, "User not found"); + } + + if (user.otp !== otp || user.otp_expires_at < new Date()) { + throw new HttpError(400, "Invalid OTP "); + } + + user.isverified = true; + await AppDataSource.manager.save(user); + + return { message: "Email successfully verified" }; + } catch (error) { + if (error.name === "TokenExpiredError") { + throw new HttpError(400, "Verification token has expired"); + } + throw new HttpError(error.status || 500, error.message || error); + } + } + + public async login( + payload: IUserLogin, + ): Promise<{ access_token: string; user: Partial }> { + const { email, password } = payload; + + try { + const user = await User.findOne({ + where: { email }, + relations: ["profile"], + }); + + if (!user) { + throw new HttpError(404, "User not found"); + } + + if (user.google_id && user.password === null) { + throw new HttpError(401, "User Created with Google"); + } + + const isPasswordValid = await comparePassword(password, user.password); + if (!isPasswordValid) { + throw new HttpError(401, "Invalid credentials"); + } + + if (!user.isverified) { + throw new HttpError(403, "Email not verified"); + } + + const access_token = jwt.sign({ userId: user.id }, config.TOKEN_SECRET, { + expiresIn: "1d", + }); + + const { password: _, ...userWithoutPassword } = user; + + return { access_token, user: userWithoutPassword }; + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + } + + resetPassword = async ( + token: string, + newPassword: string, + ): Promise<{ message: string }> => { + try { + const hashedToken = crypto + .createHash("sha256") + .update(token) + .digest("hex"); + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(newPassword, salt); + + const user = await User.findOne({ + where: { + passwordResetToken: hashedToken, + passwordResetExpires: MoreThan(Date.now()), + }, + }); + + if (!user) { + throw new HttpError(404, "Token is invalid or has expired"); + } + + user.password = hashedPassword; + user.passwordResetToken = undefined; + user.passwordResetExpires = undefined; + + await AppDataSource.manager.save(user); + + return { message: "Password reset successfully." }; + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + }; + + public async forgotPassword( + email: string, + resetURL: string, + ): Promise<{ message: string }> { + try { + const user = await User.findOne({ where: { email } }); + + if (!user) { + throw new HttpError(404, "User not found"); + } + + const resetToken = user.createPasswordResetToken(); + await AppDataSource.manager.save(user); + + const htmlTemplate = renderTemplate("password-reset", { + title: "Reset Password", + userName: user.name, + resetUrl: resetURL + `${resetToken}`, + }); + + const emailContent = { + from: `Boilerplate <${config.SMTP_USER}>`, + to: email, + subject: "Password Reset", + html: htmlTemplate, + }; + + await addEmailToQueue(emailContent); + + return { message: "Password reset link sent successfully." }; + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + } + + public async changePassword( + userId: string, + oldPassword: string, + newPassword: string, + confirmPassword: string, + ): Promise<{ message: string }> { + try { + const user = await User.findOne({ where: { id: userId } }); + + if (!user) { + throw new HttpError(404, "User not found"); + } + + const isOldPasswordValid = await comparePassword( + oldPassword, + user.password, + ); + if (!isOldPasswordValid) { + throw new HttpError(401, "Old password is incorrect"); + } + + if (oldPassword === newPassword) { + throw new HttpError( + 400, + "You used this password recently. Please choose a different one.", + ); + } + + if (newPassword !== confirmPassword) { + throw new HttpError(400, "New password and confirmation do not match"); + } + + user.password = await hashPassword(newPassword); + await AppDataSource.manager.save(user); + + return { message: "Password changed successfully" }; + } catch (error) { + throw error; + } + } + + public async generateMagicLink(email: string) { + try { + const user = await User.findOne({ where: { email } }); + if (user === null || !user) { + throw new ResourceNotFound("User is not registered"); + } + + const token = generateToken({ email: email }); + const protocol = APP_CONFIG.USE_HTTPS ? "https" : "http"; + const magicLinkUrl = `${protocol}://${config.BASE_URL}/api/v1/auth/magic-link?token=${token}`; + + const mailToBeSentToUser = await Sendmail({ + from: `Boilerplate `, + to: email, + subject: "MAGIC LINK LOGIN", + html: generateMagicLinkEmail(magicLinkUrl, email), + }); + + return { + ok: mailToBeSentToUser === "Email sent successfully.", + message: "Email sent successfully.", + user, + }; + } catch (err) { + throw err; + } + } + + public async validateMagicLinkToken(token: string) { + try { + const { email } = verifyToken(token as string); + if (!email) { + throw new BadRequest("Invalid JWT"); + } + + const user = await User.findOne({ + where: { email: String(email) }, + }); + + if (user === null || !user) { + throw new ResourceNotFound("User not found"); + } + + return { + status: "ok", + email: user.email, + userId: user.id, + }; + } catch (error) { + throw error; + } + } + + public async passwordlessLogin(userId: string) { + try { + const access_token = await generateAccessToken(userId); + + return { + access_token, + }; + } catch (error) { + throw error; + } + } + + public generate2FARecoveryCode() { + const codes = []; + for (let i = 0; i < 8; i++) { + codes.push(Math.random().toString(36).substring(2, 8).toUpperCase()); + } + return codes; + } + + public async enable2FA(user_id: string, password: string) { + try { + const user = await this.userService.getUserById(user_id); + + const is_password_valid = await this.userService.compareUserPassword( + password, + user.password, + ); + if (!is_password_valid) { + throw new BadRequest("Invalid password"); + } + if (user.is_2fa_enabled) { + throw new BadRequest("2FA is already enabled"); + } + + const secret = speakeasy.generateSecret({ length: 32 }); + const backup_codes = this.generate2FARecoveryCode(); + const payload = { + secret: secret.base32, + is_2fa_enabled: true, + backup_codes: backup_codes, + }; + + await this.userService.updateUserRecord({ + updatePayload: payload, + identifierOption: { + identifier: user_id, + identifierType: "id", + }, + }); + + const qrCodeUrl = speakeasy.otpauthURL({ + secret: secret.ascii, + label: `Hng:${user.email}`, + issuer: `Hng Boilerplate`, + }); + + return { + message: "2FA setup initiated", + data: { + secret: secret.base32, + qr_code_url: qrCodeUrl, + backup_codes, + }, + }; + } catch (error) { + if (error instanceof BadRequest) { + throw error; + } + throw new ServerError("An error occurred while trying to enable 2FA"); + } + } + + public verify2FA(totp_code: string, user: User) { + return speakeasy.totp.verify({ + secret: user.secret, + encoding: "base32", + token: totp_code, + }); + } +} diff --git a/src/services/billing-plans.services.ts b/src/services/billing-plans.services.ts index a670511a..06ec2528 100644 --- a/src/services/billing-plans.services.ts +++ b/src/services/billing-plans.services.ts @@ -1,15 +1,15 @@ -import { NextFunction, Request, Response } from "express"; -import AppDataSource from "../data-source"; -import { BillingPlan } from "../models/billing-plan"; - -export class BillingService { - private billingRepository = AppDataSource.getRepository(BillingPlan); - - public async getAllBillingPlans(): Promise { - try { - return await this.billingRepository.find(); - } catch (error) { - throw new Error("Could not fetch billing plans"); - } - } -} +import { NextFunction, Request, Response } from "express"; +import AppDataSource from "../data-source"; +import { BillingPlan } from "../models/billing-plan"; + +export class BillingService { + private billingRepository = AppDataSource.getRepository(BillingPlan); + + public async getAllBillingPlans(): Promise { + try { + return await this.billingRepository.find(); + } catch (error) { + throw new Error("Could not fetch billing plans"); + } + } +} diff --git a/src/services/billingplan.services.ts b/src/services/billingplan.services.ts index 6e62aa30..02b00903 100644 --- a/src/services/billingplan.services.ts +++ b/src/services/billingplan.services.ts @@ -1,56 +1,56 @@ -import { Repository } from "typeorm"; -import { IBillingPlanService } from "../types"; -import { BillingPlan } from "../models/billing-plan"; -import AppDataSource from "../data-source"; -import { Organization } from "../models"; -import { ResourceNotFound } from "../middleware"; - -export class BillingPlanService implements IBillingPlanService { - private billingplanRepository: Repository; - - constructor() { - this.billingplanRepository = AppDataSource.getRepository(BillingPlan); - } - async createBillingPlan( - planData: Partial, - ): Promise { - if (!planData.organizationId) { - throw new Error("Organization ID is required."); - } - - const organization = await AppDataSource.getRepository( - Organization, - ).findOne({ - where: { id: planData.organizationId }, - }); - - if (!organization) { - throw new Error("Organization does not exist."); - } - - const newPlan = this.billingplanRepository.create({ - id: planData.id, - name: planData.name, - price: planData.price, - organizationId: planData.organizationId, - currency: "USD", - duration: "monthly", - features: [], - }); - - await this.billingplanRepository.save(newPlan); - - return [newPlan]; - } - - async getBillingPlan(planId: string): Promise { - const billingPlan = await this.billingplanRepository.findOne({ - where: { id: planId }, - }); - if (!billingPlan) { - throw new ResourceNotFound(`Billing plan with ID ${planId} not found`); - } - - return billingPlan; - } -} +import { Repository } from "typeorm"; +import { IBillingPlanService } from "../types"; +import { BillingPlan } from "../models/billing-plan"; +import AppDataSource from "../data-source"; +import { Organization } from "../models"; +import { ResourceNotFound } from "../middleware"; + +export class BillingPlanService implements IBillingPlanService { + private billingplanRepository: Repository; + + constructor() { + this.billingplanRepository = AppDataSource.getRepository(BillingPlan); + } + async createBillingPlan( + planData: Partial, + ): Promise { + if (!planData.organizationId) { + throw new Error("Organization ID is required."); + } + + const organization = await AppDataSource.getRepository( + Organization, + ).findOne({ + where: { id: planData.organizationId }, + }); + + if (!organization) { + throw new Error("Organization does not exist."); + } + + const newPlan = this.billingplanRepository.create({ + id: planData.id, + name: planData.name, + price: planData.price, + organizationId: planData.organizationId, + currency: "USD", + duration: "monthly", + features: [], + }); + + await this.billingplanRepository.save(newPlan); + + return [newPlan]; + } + + async getBillingPlan(planId: string): Promise { + const billingPlan = await this.billingplanRepository.findOne({ + where: { id: planId }, + }); + if (!billingPlan) { + throw new ResourceNotFound(`Billing plan with ID ${planId} not found`); + } + + return billingPlan; + } +} diff --git a/src/services/blogCategory.services.ts b/src/services/blogCategory.services.ts index 648abaf4..60f92b53 100644 --- a/src/services/blogCategory.services.ts +++ b/src/services/blogCategory.services.ts @@ -1,11 +1,11 @@ -import AppDataSource from "../data-source"; -import { Category } from "../models/category"; - -const categoryRepository = AppDataSource.getRepository(Category); - -export const createCategory = async (name: string) => { - const newCategory = new Category(); - newCategory.name = name; - - return categoryRepository.save(newCategory); -}; +import AppDataSource from "../data-source"; +import { Category } from "../models/category"; + +const categoryRepository = AppDataSource.getRepository(Category); + +export const createCategory = async (name: string) => { + const newCategory = new Category(); + newCategory.name = name; + + return categoryRepository.save(newCategory); +}; diff --git a/src/services/blogLike.services.ts b/src/services/blogLike.services.ts index ae176b27..a047ec09 100644 --- a/src/services/blogLike.services.ts +++ b/src/services/blogLike.services.ts @@ -1,21 +1,21 @@ -import AppDataSource from "../data-source"; -import { Like } from "../models/like"; -import { Blog } from "../models/blog"; -import { User } from "../models/user"; - -const likeRepository = AppDataSource.getRepository(Like); -const blogRepository = AppDataSource.getRepository(Blog); -const userRepository = AppDataSource.getRepository(User); - -export const addLike = async (blogId: string, userId: string) => { - const blog = await blogRepository.findOneBy({ id: blogId }); - const user = await userRepository.findOneBy({ id: userId }); - - if (!blog || !user) throw new Error("Blog or User not found"); - - const newLike = new Like(); - newLike.blog = blog; - newLike.user = user; - - return likeRepository.save(newLike); -}; +import AppDataSource from "../data-source"; +import { Like } from "../models/like"; +import { Blog } from "../models/blog"; +import { User } from "../models/user"; + +const likeRepository = AppDataSource.getRepository(Like); +const blogRepository = AppDataSource.getRepository(Blog); +const userRepository = AppDataSource.getRepository(User); + +export const addLike = async (blogId: string, userId: string) => { + const blog = await blogRepository.findOneBy({ id: blogId }); + const user = await userRepository.findOneBy({ id: userId }); + + if (!blog || !user) throw new Error("Blog or User not found"); + + const newLike = new Like(); + newLike.blog = blog; + newLike.user = user; + + return likeRepository.save(newLike); +}; diff --git a/src/services/blogTag.services.ts b/src/services/blogTag.services.ts index 7dd53341..11aeffe3 100644 --- a/src/services/blogTag.services.ts +++ b/src/services/blogTag.services.ts @@ -1,11 +1,11 @@ -import AppDataSource from "../data-source"; -import { Tag } from "../models/tag"; - -const tagRepository = AppDataSource.getRepository(Tag); - -export const createTag = async (name: string) => { - const newTag = new Tag(); - newTag.name = name; - - return tagRepository.save(newTag); -}; +import AppDataSource from "../data-source"; +import { Tag } from "../models/tag"; + +const tagRepository = AppDataSource.getRepository(Tag); + +export const createTag = async (name: string) => { + const newTag = new Tag(); + newTag.name = name; + + return tagRepository.save(newTag); +}; diff --git a/src/services/contactService.ts b/src/services/contactService.ts index ffb9b055..ec1e84c1 100644 --- a/src/services/contactService.ts +++ b/src/services/contactService.ts @@ -1,16 +1,16 @@ -import AppDataSource from "../data-source"; -import { Contact } from "../models/contact"; - -export class ContactService { - private contactRepository = AppDataSource.getRepository(Contact); - - async createContact(contactData: Partial): Promise { - const contact = this.contactRepository.create(contactData); - return this.contactRepository.save(contact); - } - - async getAllContactUs(): Promise { - const contacts = await this.contactRepository.find(); - return contacts; - } -} +import AppDataSource from "../data-source"; +import { Contact } from "../models/contact"; + +export class ContactService { + private contactRepository = AppDataSource.getRepository(Contact); + + async createContact(contactData: Partial): Promise { + const contact = this.contactRepository.create(contactData); + return this.contactRepository.save(contact); + } + + async getAllContactUs(): Promise { + const contacts = await this.contactRepository.find(); + return contacts; + } +} diff --git a/src/services/export.services.ts b/src/services/export.services.ts index b68695ea..745be659 100644 --- a/src/services/export.services.ts +++ b/src/services/export.services.ts @@ -1,42 +1,42 @@ -import AppDataSource from "../data-source"; -import { User } from "../models/user"; -import { Parser } from "json2csv"; -import PDFDocument from "pdfkit"; -import { PassThrough } from "stream"; - -class ExportService { - static async getUserById(id: string) { - const userRepository = AppDataSource.getRepository(User); - return userRepository.findOne({ where: { id } }); - } - - static generateCSV(users: User[]): string { - const json2csvParser = new Parser(); - return json2csvParser.parse(users); - } - - static async generatePDF(users: User[]): Promise { - return new Promise((resolve, reject) => { - const doc = new PDFDocument(); - const stream = new PassThrough(); - const buffers: Buffer[] = []; - - doc.pipe(stream); - - stream.on("data", (chunk) => buffers.push(chunk)); - stream.on("end", () => resolve(Buffer.concat(buffers))); - stream.on("error", reject); - - users.forEach((user) => { - doc.text(`ID: ${user.id}`); - doc.text(`Name: ${user.name}`); - doc.text(`Email: ${user.email}`); - doc.moveDown(); - }); - - doc.end(); - }); - } -} - -export default ExportService; +import AppDataSource from "../data-source"; +import { User } from "../models/user"; +import { Parser } from "json2csv"; +import PDFDocument from "pdfkit"; +import { PassThrough } from "stream"; + +class ExportService { + static async getUserById(id: string) { + const userRepository = AppDataSource.getRepository(User); + return userRepository.findOne({ where: { id } }); + } + + static generateCSV(users: User[]): string { + const json2csvParser = new Parser(); + return json2csvParser.parse(users); + } + + static async generatePDF(users: User[]): Promise { + return new Promise((resolve, reject) => { + const doc = new PDFDocument(); + const stream = new PassThrough(); + const buffers: Buffer[] = []; + + doc.pipe(stream); + + stream.on("data", (chunk) => buffers.push(chunk)); + stream.on("end", () => resolve(Buffer.concat(buffers))); + stream.on("error", reject); + + users.forEach((user) => { + doc.text(`ID: ${user.id}`); + doc.text(`Name: ${user.name}`); + doc.text(`Email: ${user.email}`); + doc.moveDown(); + }); + + doc.end(); + }); + } +} + +export default ExportService; diff --git a/src/services/faq.services.ts b/src/services/faq.services.ts index 62ae0edb..b39358a7 100644 --- a/src/services/faq.services.ts +++ b/src/services/faq.services.ts @@ -1,78 +1,78 @@ -import AppDataSource from "../data-source"; -import { FAQ } from "../models/faq"; -import { Repository } from "typeorm"; -import { - BadRequest, - HttpError, - ResourceNotFound, - Unauthorized, -} from "../middleware"; - -type FAQType = { - question: string; - answer: string; - category: string; - createdBy: string; -}; - -class FAQService { - private faqRepository: Repository; - constructor() { - this.faqRepository = AppDataSource.getRepository(FAQ); - } - public async createFaq(data: FAQType): Promise { - try { - const faq = this.faqRepository.create(data); - const createdFAQ = await this.faqRepository.save(faq); - return createdFAQ; - } catch (error) { - throw new Error("Failed to create FAQ"); - } - } - - public async updateFaq(payload: Partial, faqId: string) { - const faq = await this.faqRepository.findOne({ where: { id: faqId } }); - - if (!faq) { - throw new BadRequest(`Invalid request data`); - } - - Object.assign(faq, payload); - - try { - await this.faqRepository.update(faqId, payload); - const updatedFaq = await this.faqRepository.findOne({ - where: { id: faqId }, - }); - return updatedFaq; - } catch (error) { - throw error; - } - } - - public async getAllFaqs(): Promise { - try { - const faqs = await this.faqRepository.find(); - return faqs; - } catch (error) { - throw new Error("Failed to fetch FAQs"); - } - } - - public async deleteFaq(faqId: string) { - const faq = await this.faqRepository.findOne({ where: { id: faqId } }); - - if (!faq) { - throw new BadRequest(`Invalid request data`); - } - - try { - const result = await this.faqRepository.delete(faqId); - return result.affected !== 0; - } catch (error) { - throw new HttpError(500, "Deletion failed"); - } - } -} - -export { FAQService }; +import AppDataSource from "../data-source"; +import { FAQ } from "../models/faq"; +import { Repository } from "typeorm"; +import { + BadRequest, + HttpError, + ResourceNotFound, + Unauthorized, +} from "../middleware"; + +type FAQType = { + question: string; + answer: string; + category: string; + createdBy: string; +}; + +class FAQService { + private faqRepository: Repository; + constructor() { + this.faqRepository = AppDataSource.getRepository(FAQ); + } + public async createFaq(data: FAQType): Promise { + try { + const faq = this.faqRepository.create(data); + const createdFAQ = await this.faqRepository.save(faq); + return createdFAQ; + } catch (error) { + throw new Error("Failed to create FAQ"); + } + } + + public async updateFaq(payload: Partial, faqId: string) { + const faq = await this.faqRepository.findOne({ where: { id: faqId } }); + + if (!faq) { + throw new BadRequest(`Invalid request data`); + } + + Object.assign(faq, payload); + + try { + await this.faqRepository.update(faqId, payload); + const updatedFaq = await this.faqRepository.findOne({ + where: { id: faqId }, + }); + return updatedFaq; + } catch (error) { + throw error; + } + } + + public async getAllFaqs(): Promise { + try { + const faqs = await this.faqRepository.find(); + return faqs; + } catch (error) { + throw new Error("Failed to fetch FAQs"); + } + } + + public async deleteFaq(faqId: string) { + const faq = await this.faqRepository.findOne({ where: { id: faqId } }); + + if (!faq) { + throw new BadRequest(`Invalid request data`); + } + + try { + const result = await this.faqRepository.delete(faqId); + return result.affected !== 0; + } catch (error) { + throw new HttpError(500, "Deletion failed"); + } + } +} + +export { FAQService }; diff --git a/src/services/google.auth.service.ts b/src/services/google.auth.service.ts index fb0d04d8..fc276ae5 100644 --- a/src/services/google.auth.service.ts +++ b/src/services/google.auth.service.ts @@ -1,39 +1,39 @@ -import AppDataSource from "../data-source"; -import { Profile, User } from "../models"; - -export async function GoogleUserInfo(userInfo: any) { - const { sub: google_id, email, name, picture, email_verified } = userInfo; - - // const userRepository = getRepository(User); - - let user = await AppDataSource.getRepository(User).findOne({ - where: { email }, - relations: ["profile"], - }); - - const [first_name = "", last_name = ""] = name.split(" "); - - if (!user) { - user = new User(); - user.google_id = google_id; - user.email = email; - user.name = name; - user.isverified = email_verified; - user.profile = new Profile(); - user.profile.first_name = first_name; - user.profile.last_name = last_name; - user.profile.avatarUrl = picture; - } - - await AppDataSource.manager.save(user); - const { - password: _, - otp: __, - otp_expires_at, - createdAt, - updatedAt, - deletedAt, - ...rest - } = user; - return rest; -} +import AppDataSource from "../data-source"; +import { Profile, User } from "../models"; + +export async function GoogleUserInfo(userInfo: any) { + const { sub: google_id, email, name, picture, email_verified } = userInfo; + + // const userRepository = getRepository(User); + + let user = await AppDataSource.getRepository(User).findOne({ + where: { email }, + relations: ["profile"], + }); + + const [first_name = "", last_name = ""] = name.split(" "); + + if (!user) { + user = new User(); + user.google_id = google_id; + user.email = email; + user.name = name; + user.isverified = email_verified; + user.profile = new Profile(); + user.profile.first_name = first_name; + user.profile.last_name = last_name; + user.profile.avatarUrl = picture; + } + + await AppDataSource.manager.save(user); + const { + password: _, + otp: __, + otp_expires_at, + createdAt, + updatedAt, + deletedAt, + ...rest + } = user; + return rest; +} diff --git a/src/services/help.services.ts b/src/services/help.services.ts index 87e66c7f..4694c1fa 100644 --- a/src/services/help.services.ts +++ b/src/services/help.services.ts @@ -1,167 +1,167 @@ -// src/services/HelpService.ts -import { NextFunction, Request, Response } from "express"; -import jwt from "jsonwebtoken"; -import { HelpCenterTopic } from "../models"; -import { User } from "../models"; -import AppDataSource from "../data-source"; -import { HttpError } from "../middleware"; -import config from "../config"; -import { DeleteResult, Repository } from "typeorm"; - -export class HelpService { - private helpRepository: Repository; - - constructor() { - this.helpRepository = AppDataSource.getRepository(HelpCenterTopic); - } - - public async create( - title: string, - content: string, - author: string, - ): Promise { - try { - //Check for Existing Title - const existingTitle = await this.helpRepository.findOne({ - where: { title }, - }); - if (existingTitle) { - throw new HttpError(422, "Article already exists"); - } - - const articleEntity = this.helpRepository.create({ - title, - content, - author, - }); - const article = await this.helpRepository.save(articleEntity); - return article; - } catch (error) { - throw new HttpError(error.status_code, error.message || error); - } - } - - public async getAll(): Promise { - try { - const articles = await this.helpRepository.find(); - return articles; - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - } - - public async update( - id: string, - title: string, - content: string, - author: string, - ): Promise { - try { - const article_id = id; - - // Check if article exists - const existingArticle = await this.helpRepository.findOne({ - where: { id: article_id }, - }); - - if (!existingArticle) { - throw new HttpError(404, "Not Found"); - } - - //Update Article on DB - await this.helpRepository.update(article_id, { title, content, author }); - - //Fetch Updated article - const newArticle = await this.helpRepository.findOne({ - where: { id: article_id }, - }); - return newArticle; - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - } - - public async getTopicById(id: string): Promise { - try { - const article_id = id; - - // Check if article exists - const existingArticle = await this.helpRepository.findOne({ - where: { id: article_id }, - }); - - if (!existingArticle) { - throw new HttpError(404, "Not Found"); - } - - //Fetch Updated article - const article = await this.helpRepository.findOne({ - where: { id: article_id }, - }); - - return article; - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - } - - public async delete(id: string): Promise { - try { - const article_id = id; - - // Check if article exists - const existingArticle = await this.helpRepository.findOne({ - where: { id: article_id }, - }); - - if (!existingArticle) { - throw new HttpError(404, "Not Found"); - } - - //Delete article - const article = await this.helpRepository.delete({ id: article_id }); - return article; - } catch (error) { - throw new HttpError(error.status || 500, error.message || error); - } - } -} - -export const verifyAdmin = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - const authHeader = req.headers["authorization"]; - const token = authHeader && authHeader.split(" ")[1]; - try { - const decodedToken = jwt.decode(token); - - if (typeof decodedToken === "string" || !decodedToken) { - return res.status(401).json({ - success: false, - message: "Access denied. Invalid token", - status_code: 401, - }); - } - - const userRepository = AppDataSource.getRepository(User); - const user = await userRepository.findOne({ - where: { id: decodedToken.userId }, - }); - - if (user.role !== "super_admin") { - return res.status(403).json({ - success: false, - message: "Access denied! You are not an admin", - status_code: 403, - }); - } - - next(); - } catch (error) { - res - .status(401) - .json({ status: "error", message: "Access denied. Invalid token" }); - } -}; +// src/services/HelpService.ts +import { NextFunction, Request, Response } from "express"; +import jwt from "jsonwebtoken"; +import { HelpCenterTopic } from "../models"; +import { User } from "../models"; +import AppDataSource from "../data-source"; +import { HttpError } from "../middleware"; +import config from "../config"; +import { DeleteResult, Repository } from "typeorm"; + +export class HelpService { + private helpRepository: Repository; + + constructor() { + this.helpRepository = AppDataSource.getRepository(HelpCenterTopic); + } + + public async create( + title: string, + content: string, + author: string, + ): Promise { + try { + //Check for Existing Title + const existingTitle = await this.helpRepository.findOne({ + where: { title }, + }); + if (existingTitle) { + throw new HttpError(422, "Article already exists"); + } + + const articleEntity = this.helpRepository.create({ + title, + content, + author, + }); + const article = await this.helpRepository.save(articleEntity); + return article; + } catch (error) { + throw new HttpError(error.status_code, error.message || error); + } + } + + public async getAll(): Promise { + try { + const articles = await this.helpRepository.find(); + return articles; + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + } + + public async update( + id: string, + title: string, + content: string, + author: string, + ): Promise { + try { + const article_id = id; + + // Check if article exists + const existingArticle = await this.helpRepository.findOne({ + where: { id: article_id }, + }); + + if (!existingArticle) { + throw new HttpError(404, "Not Found"); + } + + //Update Article on DB + await this.helpRepository.update(article_id, { title, content, author }); + + //Fetch Updated article + const newArticle = await this.helpRepository.findOne({ + where: { id: article_id }, + }); + return newArticle; + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + } + + public async getTopicById(id: string): Promise { + try { + const article_id = id; + + // Check if article exists + const existingArticle = await this.helpRepository.findOne({ + where: { id: article_id }, + }); + + if (!existingArticle) { + throw new HttpError(404, "Not Found"); + } + + //Fetch Updated article + const article = await this.helpRepository.findOne({ + where: { id: article_id }, + }); + + return article; + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + } + + public async delete(id: string): Promise { + try { + const article_id = id; + + // Check if article exists + const existingArticle = await this.helpRepository.findOne({ + where: { id: article_id }, + }); + + if (!existingArticle) { + throw new HttpError(404, "Not Found"); + } + + //Delete article + const article = await this.helpRepository.delete({ id: article_id }); + return article; + } catch (error) { + throw new HttpError(error.status || 500, error.message || error); + } + } +} + +export const verifyAdmin = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + try { + const decodedToken = jwt.decode(token); + + if (typeof decodedToken === "string" || !decodedToken) { + return res.status(401).json({ + success: false, + message: "Access denied. Invalid token", + status_code: 401, + }); + } + + const userRepository = AppDataSource.getRepository(User); + const user = await userRepository.findOne({ + where: { id: decodedToken.userId }, + }); + + if (user.role !== "super_admin") { + return res.status(403).json({ + success: false, + message: "Access denied! You are not an admin", + status_code: 403, + }); + } + + next(); + } catch (error) { + res + .status(401) + .json({ status: "error", message: "Access denied. Invalid token" }); + } +}; diff --git a/src/services/newsLetterSubscription.service.ts b/src/services/newsLetterSubscription.service.ts index 24052042..07c5f7c7 100644 --- a/src/services/newsLetterSubscription.service.ts +++ b/src/services/newsLetterSubscription.service.ts @@ -1,100 +1,100 @@ -import { Repository } from "typeorm"; -import AppDataSource from "../data-source"; -import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; -import { INewsLetterSubscriptionService } from "../types"; -import { BadRequest, HttpError, ResourceNotFound } from "../middleware"; - -export class NewsLetterSubscriptionService - implements INewsLetterSubscriptionService -{ - private newsLetterSubscriber: Repository; - - constructor() { - this.newsLetterSubscriber = - AppDataSource.getRepository(NewsLetterSubscriber); - } - - public async subscribeUser(email: string): Promise<{ - isNewlySubscribe: boolean; - subscriber: NewsLetterSubscriber; - }> { - let isNewlySubscribe = true; - - const isExistingSubscriber = await this.newsLetterSubscriber.findOne({ - where: { email }, - }); - if (isExistingSubscriber && isExistingSubscriber.isSubscribe === true) { - isNewlySubscribe = false; - return { isNewlySubscribe, subscriber: isExistingSubscriber }; - } - - if (isExistingSubscriber && isExistingSubscriber.isSubscribe === false) { - throw new BadRequest( - "You are already subscribed, please enable newsletter subscription to receive newsletter again", - ); - } - - const newSubscriber = new NewsLetterSubscriber(); - newSubscriber.email = email; - newSubscriber.isSubscribe = true; - - const subscriber = await this.newsLetterSubscriber.save(newSubscriber); - - if (!subscriber) { - throw new HttpError( - 500, - "An error occurred while processing your request", - ); - } - return { isNewlySubscribe, subscriber }; - } - - public async unSubcribeUser(email: string): Promise { - const isExistingSubscriber = await this.newsLetterSubscriber.findOne({ - where: { email }, - }); - - if (!isExistingSubscriber) { - throw new ResourceNotFound("You are not subscribed to newsletter"); - } - - if (isExistingSubscriber && isExistingSubscriber.isSubscribe === true) { - isExistingSubscriber.isSubscribe = false; - await this.newsLetterSubscriber.save(isExistingSubscriber); - return isExistingSubscriber; - } - - throw new BadRequest("You already unsubscribed to newsletter"); - } - - public async fetchAllNewsletter({ - page = 1, - limit = 10, - }: { - page?: number; - limit?: number; - }) { - try { - const [newsletters, total] = await this.newsLetterSubscriber.findAndCount( - { - skip: (page - 1) * limit, - take: limit, - }, - ); - const totalPages = Math.ceil(total / limit); - const meta = { - total, - page, - limit, - totalPages, - }; - - return { - data: newsletters, - meta, - }; - } catch (error) { - throw error; - } - } -} +import { Repository } from "typeorm"; +import AppDataSource from "../data-source"; +import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; +import { INewsLetterSubscriptionService } from "../types"; +import { BadRequest, HttpError, ResourceNotFound } from "../middleware"; + +export class NewsLetterSubscriptionService + implements INewsLetterSubscriptionService +{ + private newsLetterSubscriber: Repository; + + constructor() { + this.newsLetterSubscriber = + AppDataSource.getRepository(NewsLetterSubscriber); + } + + public async subscribeUser(email: string): Promise<{ + isNewlySubscribe: boolean; + subscriber: NewsLetterSubscriber; + }> { + let isNewlySubscribe = true; + + const isExistingSubscriber = await this.newsLetterSubscriber.findOne({ + where: { email }, + }); + if (isExistingSubscriber && isExistingSubscriber.isSubscribe === true) { + isNewlySubscribe = false; + return { isNewlySubscribe, subscriber: isExistingSubscriber }; + } + + if (isExistingSubscriber && isExistingSubscriber.isSubscribe === false) { + throw new BadRequest( + "You are already subscribed, please enable newsletter subscription to receive newsletter again", + ); + } + + const newSubscriber = new NewsLetterSubscriber(); + newSubscriber.email = email; + newSubscriber.isSubscribe = true; + + const subscriber = await this.newsLetterSubscriber.save(newSubscriber); + + if (!subscriber) { + throw new HttpError( + 500, + "An error occurred while processing your request", + ); + } + return { isNewlySubscribe, subscriber }; + } + + public async unSubcribeUser(email: string): Promise { + const isExistingSubscriber = await this.newsLetterSubscriber.findOne({ + where: { email }, + }); + + if (!isExistingSubscriber) { + throw new ResourceNotFound("You are not subscribed to newsletter"); + } + + if (isExistingSubscriber && isExistingSubscriber.isSubscribe === true) { + isExistingSubscriber.isSubscribe = false; + await this.newsLetterSubscriber.save(isExistingSubscriber); + return isExistingSubscriber; + } + + throw new BadRequest("You already unsubscribed to newsletter"); + } + + public async fetchAllNewsletter({ + page = 1, + limit = 10, + }: { + page?: number; + limit?: number; + }) { + try { + const [newsletters, total] = await this.newsLetterSubscriber.findAndCount( + { + skip: (page - 1) * limit, + take: limit, + }, + ); + const totalPages = Math.ceil(total / limit); + const meta = { + total, + page, + limit, + totalPages, + }; + + return { + data: newsletters, + meta, + }; + } catch (error) { + throw error; + } + } +} diff --git a/src/services/notification.services.ts b/src/services/notification.services.ts index 494919dd..a0fa0de6 100644 --- a/src/services/notification.services.ts +++ b/src/services/notification.services.ts @@ -1,29 +1,29 @@ -import { Repository } from "typeorm"; -import { Notification, User } from "../models"; -import AppDataSource from "../data-source"; - -export class NotificationsService { - private notificationRepository: Repository; - - constructor() { - this.notificationRepository = AppDataSource.getRepository(Notification); // Inject the repository - } - - public async getNotificationsForUser(userId: string): Promise { - const notifications = await this.notificationRepository.find({ - where: { user: { id: userId } }, - order: { createdAt: "DESC" }, - }); - - const totalNotificationCount = notifications.length; - const totalUnreadNotificationCount = notifications.filter( - (notification) => !notification.isRead, - ).length; - - return { - totalNotificationCount, - totalUnreadNotificationCount, - notifications, - }; - } -} +import { Repository } from "typeorm"; +import { Notification, User } from "../models"; +import AppDataSource from "../data-source"; + +export class NotificationsService { + private notificationRepository: Repository; + + constructor() { + this.notificationRepository = AppDataSource.getRepository(Notification); // Inject the repository + } + + public async getNotificationsForUser(userId: string): Promise { + const notifications = await this.notificationRepository.find({ + where: { user: { id: userId } }, + order: { createdAt: "DESC" }, + }); + + const totalNotificationCount = notifications.length; + const totalUnreadNotificationCount = notifications.filter( + (notification) => !notification.isRead, + ).length; + + return { + totalNotificationCount, + totalUnreadNotificationCount, + notifications, + }; + } +} diff --git a/src/services/org.services.ts b/src/services/org.services.ts index f708bb06..fbf4aba6 100644 --- a/src/services/org.services.ts +++ b/src/services/org.services.ts @@ -1,531 +1,531 @@ -import { Repository } from "typeorm"; -import { v4 as uuidv4 } from "uuid"; -import config from "../config/index"; -import AppDataSource from "../data-source"; -import { UserRole } from "../enums/userRoles"; -import { - BadRequest, - ResourceNotFound, - HttpError, - Conflict, -} from "../middleware"; -import { Organization, Invitation, UserOrganization } from "../models"; -import { OrganizationRole } from "../models/organization-role.entity"; -import { User } from "../models/user"; -import { ICreateOrganisation, ICreateOrgRole, IOrgService } from "../types"; -import log from "../utils/logger"; - -import { addEmailToQueue } from "../utils/queue"; -import renderTemplate from "../views/email/renderTemplate"; -import { PermissionCategory } from "../enums/permission-category.enum"; -import { Permissions } from "../models/permissions.entity"; -const frontendBaseUrl = config.BASE_URL; - -export class OrgService implements IOrgService { - private organizationRepository: Repository; - private organizationRoleRepository: Repository; - private permissionRepository: Repository; - - constructor() { - this.organizationRepository = AppDataSource.getRepository(Organization); - this.organizationRoleRepository = - AppDataSource.getRepository(OrganizationRole); - } - public async createOrganisation( - payload: ICreateOrganisation, - userId: string, - ): Promise<{ - new_organisation: Partial; - }> { - try { - const organisation = new Organization(); - organisation.owner_id = userId; - Object.assign(organisation, payload); - - const new_organisation = await AppDataSource.manager.save(organisation); - - const userOrganization = new UserOrganization(); - userOrganization.userId = userId; - userOrganization.organizationId = new_organisation.id; - userOrganization.role = UserRole.ADMIN; - - await AppDataSource.manager.save(userOrganization); - - return { new_organisation }; - } catch (error) { - throw new BadRequest("Client error"); - } - } - - public async removeUser( - org_id: string, - user_id: string, - ): Promise { - const userOrganizationRepository = - AppDataSource.getRepository(UserOrganization); - const organizationRepository = AppDataSource.getRepository(Organization); - const userRepository = AppDataSource.getRepository(User); - - try { - const userOrganization = await userOrganizationRepository.findOne({ - where: { userId: user_id, organizationId: org_id }, - relations: ["user", "organization"], - }); - - if (!userOrganization) { - return null; - } - - await userOrganizationRepository.remove(userOrganization); - - const organization = await organizationRepository.findOne({ - where: { id: org_id, owner_id: user_id }, - relations: ["users"], - }); - - if (organization) { - organization.users = organization.users.filter( - (user) => user.id !== user_id, - ); - await organizationRepository.save(organization); - } - if (!organization) { - return null; - } - return userOrganization.user; - } catch (error) { - throw new Error("Failed to remove user from organization"); - } - } - - public async getOrganizationsByUserId( - user_id: string, - ): Promise { - try { - const userOrganizationRepository = - AppDataSource.getRepository(UserOrganization); - - const userOrganizations = await userOrganizationRepository.find({ - where: { userId: user_id }, - relations: ["organization"], - }); - - const organization = userOrganizations.map((org) => org.organization); - - return organization; - } catch (error) { - throw new Error("Failed to fetch organizations"); - } - } - - public async getSingleOrg( - org_id: string, - user_id: string, - ): Promise { - try { - const userOrganizationRepository = - AppDataSource.getRepository(UserOrganization); - - const userOrganization = await userOrganizationRepository.findOne({ - where: { userId: user_id, organizationId: org_id }, - relations: ["organization"], - }); - - return userOrganization?.organization || null; - } catch (error) { - throw new Error("Failed to fetch organization"); - } - } - public async updateOrganizationDetails( - org_id: string, - userId: string, - update_data: Partial, - ): Promise { - const organizationRepository = AppDataSource.getRepository(Organization); - - const organization = await organizationRepository.findOne({ - where: { id: org_id, userOrganizations: { user: { id: userId } } }, - }); - - if (!organization) { - throw new ResourceNotFound(`Organization with id '${org_id}' not found`); - } - - Object.assign(organization, update_data); - - try { - await organizationRepository.update(organization.id, update_data); - return organization; - } catch (error) { - throw error; - } - } - - public async generateGenericInviteLink( - organizationId: string, - ): Promise { - const inviteRepository = AppDataSource.getRepository(Invitation); - const organizationRepository = AppDataSource.getRepository(Organization); - const organization = await organizationRepository.findOne({ - where: { id: organizationId }, - }); - if (!organization) { - throw new ResourceNotFound( - `Organization with ID ${organizationId} not found`, - ); - } - const token = uuidv4(); - - const invite = inviteRepository.create({ - token, - isGeneric: true, - organization: { id: organizationId }, - }); - - await inviteRepository.save(invite); - - return `${frontendBaseUrl}/invite?token=${token}`; - } - - async generateAndSendInviteLinks( - emails: string[], - organizationId: string, - ): Promise { - const inviteRepository = AppDataSource.getRepository(Invitation); - const organizationRepository = AppDataSource.getRepository(Organization); - const organization = await organizationRepository.findOne({ - where: { id: organizationId }, - }); - console.log("here", organization); - if (!organization) { - throw new ResourceNotFound( - `Organization with ID ${organizationId} not found`, - ); - } - - const invites = emails.map((email) => { - const token = uuidv4(); - return inviteRepository.create({ - token, - email: email, - isGeneric: false, - organization: { id: organizationId }, - }); - }); - - invites.forEach((invite) => { - const inviteLink = `${frontendBaseUrl}/invite?token=${invite.token}`; - const emailContent = { - userName: invite.email.split("@")[0], - title: "Invitation to Join Organization", - body: `

You have been invited to join ${organization.name} organization. Please use the following link to accept the invitation:

Here`, - }; - - const mailOptions = { - from: "admin@mail.com", - to: invite.email, - subject: "Invitation to Join Organization", - html: renderTemplate("custom-email", emailContent), - }; - - addEmailToQueue(mailOptions); - }); - await inviteRepository.save(invites); - } - - async addUserToOrganizationWithInvite( - token: string, - userId: string, - ): Promise { - const inviteRepository = AppDataSource.getRepository(Invitation); - const userOrganizationRepository = - AppDataSource.getRepository(UserOrganization); - const userRepository = AppDataSource.getRepository(User); - const invite = await inviteRepository.findOne({ - where: { token }, - relations: ["organization"], - }); - - if (!invite) { - throw new ResourceNotFound("Invalid or expired invite token"); - } - - const user = await userRepository.findOne({ where: { id: userId } }); - if (!user) { - throw new ResourceNotFound("Please register to join the organization."); - } - const existingMembership = await userOrganizationRepository.findOne({ - where: { - userId: user.id, - organizationId: invite.organization.id, - }, - }); - - if (existingMembership) { - throw new Conflict("User already added to organization."); - } - - const userOrganization = userOrganizationRepository.create({ - userId: user.id, - organizationId: invite.organization.id, - user: user, - organization: invite.organization, - role: user.role, - }); - await userOrganizationRepository.save(userOrganization); - - invite.isAccepted = true; - await inviteRepository.save(invite); - - return "User added to organization successfully"; - } - async getAllInvite( - page: number, - pageSize: number, - ): Promise<{ - message: string; - data: Partial[]; - total: number; - status_code: number; - }> { - const inviteRepository = AppDataSource.getRepository(Invitation); - - const [invites, total] = await inviteRepository.findAndCount({ - skip: (page - 1) * pageSize, - take: pageSize, - }); - - if (invites.length === 0) { - return { - status_code: 200, - message: "No invites yet", - data: invites, - total, - }; - } - - const sentInvites = invites.map((invite) => { - return { - id: invite.id, - token: invite.token, - isAccepted: invite.isAccepted, - isGeneric: invite.isGeneric, - organization: invite.organization, - email: invite.email, - }; - }); - - return { - status_code: 200, - message: "Successfully fetched invites", - data: sentInvites, - total, - }; - } - - public async searchOrganizationMembers(criteria: { - name?: string; - email?: string; - }): Promise { - const userOrganizationRepository = - AppDataSource.getRepository(UserOrganization); - - const { name, email } = criteria; - - const query = userOrganizationRepository - .createQueryBuilder("userOrganization") - .leftJoinAndSelect("userOrganization.user", "user") - .leftJoinAndSelect("userOrganization.organization", "organization") - .where("1=1"); - - if (name) { - query.andWhere("LOWER(user.name) LIKE LOWER(:name)", { - name: `%${name}%`, - }); - } else if (email) { - query.andWhere("LOWER(user.email) LIKE LOWER(:email)", { - email: `%${email}%`, - }); - } - - const userOrganizations = await query.getMany(); - - if (userOrganizations.length > 0) { - const organizationsMap = new Map(); - - userOrganizations.forEach((userOrg) => { - const org = userOrg.organization; - const user = userOrg.user; - - if (!organizationsMap.has(org.id)) { - organizationsMap.set(org.id, { - organizationId: org.id, - organizationName: org.name, - organizationEmail: org.email, - members: [], - }); - } - - organizationsMap.get(org.id).members.push({ - userId: user.id, - userName: user.name, - userEmail: user.email, - }); - }); - - return Array.from(organizationsMap.values()); - } - - return []; - } - - public async createOrganizationRole( - payload: ICreateOrgRole, - organizationid: string, - ) { - try { - const organization = await this.organizationRepository.findOne({ - where: { id: organizationid }, - }); - - if (!organization) { - throw new ResourceNotFound("Organization not found"); - } - - const existingRole = await this.organizationRoleRepository.findOne({ - where: { name: payload.name, organization: { id: organizationid } }, - }); - - if (existingRole) { - throw new Conflict("Role already exists"); - } - - const role = new OrganizationRole(); - Object.assign(role, { - name: payload.name, - description: payload.description, - organization: organization, - }); - const newRole = await this.organizationRoleRepository.save(role); - - const defaultPermissions = await this.permissionRepository.find(); - - const rolePermissions = defaultPermissions.map((defaultPerm) => { - const permission = new Permissions(); - permission.category = defaultPerm.category; - permission.permission_list = defaultPerm.permission_list; - permission.role = newRole; - return permission; - }); - - await this.permissionRepository.save(rolePermissions); - return newRole; - } catch (err) { - throw err; - } - } - public async fetchSingleRole( - organizationId: string, - roleId: string, - ): Promise { - try { - const organisation = await this.organizationRepository.findOne({ - where: { id: organizationId }, - }); - if (!organisation) { - throw new ResourceNotFound( - `Organisation with ID ${organizationId} not found`, - ); - } - - const role = await this.organizationRoleRepository.findOne({ - where: { id: roleId, organization: { id: organizationId } }, - relations: ["permissions"], - }); - if (!role) { - return null; - } - - return role; - } catch (error) { - throw error; - } - } - - public async fetchAllRolesInOrganization(organizationId: string) { - try { - const organization = await this.organizationRepository.findOne({ - where: { id: organizationId }, - }); - - if (!organization) { - throw new ResourceNotFound("Organization not found"); - } - - const roles = await this.organizationRoleRepository.find({ - where: { organization: { id: organizationId } }, - select: ["id", "name", "description"], - }); - - return roles; - } catch (error) { - throw error; - } - } - - public async updateRolePermissions( - roleId: string, - organizationId: string, - newPermissions: PermissionCategory[], - ) { - try { - const organization = await this.organizationRepository.findOne({ - where: { id: organizationId }, - }); - - if (!organization) { - throw new ResourceNotFound("Organization not found"); - } - - const role = await this.organizationRoleRepository.findOne({ - where: { id: roleId, organization: { id: organizationId } }, - relations: ["permissions"], - }); - - if (!role) { - throw new ResourceNotFound("Role not found"); - } - - const newPermissionsSet = new Set(newPermissions); - - role.permissions = role.permissions.filter((permission) => - newPermissionsSet.has(permission.category), - ); - - const existingCategories = new Set( - role.permissions.map((permission) => permission.category), - ); - - for (const category of newPermissions) { - if (!existingCategories.has(category)) { - const newPermission = this.permissionRepository.create({ - category, - role, - permission_list: true, - }); - role.permissions.push(newPermission); - } - } - - role.permissions = role.permissions.filter((permission) => - newPermissionsSet.has(permission.category), - ); - - await this.organizationRoleRepository.save(role); - - return role; - } catch (error) { - throw error; - } - } -} +import { Repository } from "typeorm"; +import { v4 as uuidv4 } from "uuid"; +import config from "../config/index"; +import AppDataSource from "../data-source"; +import { UserRole } from "../enums/userRoles"; +import { + BadRequest, + ResourceNotFound, + HttpError, + Conflict, +} from "../middleware"; +import { Organization, Invitation, UserOrganization } from "../models"; +import { OrganizationRole } from "../models/organization-role.entity"; +import { User } from "../models/user"; +import { ICreateOrganisation, ICreateOrgRole, IOrgService } from "../types"; +import log from "../utils/logger"; + +import { addEmailToQueue } from "../utils/queue"; +import renderTemplate from "../views/email/renderTemplate"; +import { PermissionCategory } from "../enums/permission-category.enum"; +import { Permissions } from "../models/permissions.entity"; +const frontendBaseUrl = config.BASE_URL; + +export class OrgService implements IOrgService { + private organizationRepository: Repository; + private organizationRoleRepository: Repository; + private permissionRepository: Repository; + + constructor() { + this.organizationRepository = AppDataSource.getRepository(Organization); + this.organizationRoleRepository = + AppDataSource.getRepository(OrganizationRole); + } + public async createOrganisation( + payload: ICreateOrganisation, + userId: string, + ): Promise<{ + new_organisation: Partial; + }> { + try { + const organisation = new Organization(); + organisation.owner_id = userId; + Object.assign(organisation, payload); + + const new_organisation = await AppDataSource.manager.save(organisation); + + const userOrganization = new UserOrganization(); + userOrganization.userId = userId; + userOrganization.organizationId = new_organisation.id; + userOrganization.role = UserRole.ADMIN; + + await AppDataSource.manager.save(userOrganization); + + return { new_organisation }; + } catch (error) { + throw new BadRequest("Client error"); + } + } + + public async removeUser( + org_id: string, + user_id: string, + ): Promise { + const userOrganizationRepository = + AppDataSource.getRepository(UserOrganization); + const organizationRepository = AppDataSource.getRepository(Organization); + const userRepository = AppDataSource.getRepository(User); + + try { + const userOrganization = await userOrganizationRepository.findOne({ + where: { userId: user_id, organizationId: org_id }, + relations: ["user", "organization"], + }); + + if (!userOrganization) { + return null; + } + + await userOrganizationRepository.remove(userOrganization); + + const organization = await organizationRepository.findOne({ + where: { id: org_id, owner_id: user_id }, + relations: ["users"], + }); + + if (organization) { + organization.users = organization.users.filter( + (user) => user.id !== user_id, + ); + await organizationRepository.save(organization); + } + if (!organization) { + return null; + } + return userOrganization.user; + } catch (error) { + throw new Error("Failed to remove user from organization"); + } + } + + public async getOrganizationsByUserId( + user_id: string, + ): Promise { + try { + const userOrganizationRepository = + AppDataSource.getRepository(UserOrganization); + + const userOrganizations = await userOrganizationRepository.find({ + where: { userId: user_id }, + relations: ["organization"], + }); + + const organization = userOrganizations.map((org) => org.organization); + + return organization; + } catch (error) { + throw new Error("Failed to fetch organizations"); + } + } + + public async getSingleOrg( + org_id: string, + user_id: string, + ): Promise { + try { + const userOrganizationRepository = + AppDataSource.getRepository(UserOrganization); + + const userOrganization = await userOrganizationRepository.findOne({ + where: { userId: user_id, organizationId: org_id }, + relations: ["organization"], + }); + + return userOrganization?.organization || null; + } catch (error) { + throw new Error("Failed to fetch organization"); + } + } + public async updateOrganizationDetails( + org_id: string, + userId: string, + update_data: Partial, + ): Promise { + const organizationRepository = AppDataSource.getRepository(Organization); + + const organization = await organizationRepository.findOne({ + where: { id: org_id, userOrganizations: { user: { id: userId } } }, + }); + + if (!organization) { + throw new ResourceNotFound(`Organization with id '${org_id}' not found`); + } + + Object.assign(organization, update_data); + + try { + await organizationRepository.update(organization.id, update_data); + return organization; + } catch (error) { + throw error; + } + } + + public async generateGenericInviteLink( + organizationId: string, + ): Promise { + const inviteRepository = AppDataSource.getRepository(Invitation); + const organizationRepository = AppDataSource.getRepository(Organization); + const organization = await organizationRepository.findOne({ + where: { id: organizationId }, + }); + if (!organization) { + throw new ResourceNotFound( + `Organization with ID ${organizationId} not found`, + ); + } + const token = uuidv4(); + + const invite = inviteRepository.create({ + token, + isGeneric: true, + organization: { id: organizationId }, + }); + + await inviteRepository.save(invite); + + return `${frontendBaseUrl}/invite?token=${token}`; + } + + async generateAndSendInviteLinks( + emails: string[], + organizationId: string, + ): Promise { + const inviteRepository = AppDataSource.getRepository(Invitation); + const organizationRepository = AppDataSource.getRepository(Organization); + const organization = await organizationRepository.findOne({ + where: { id: organizationId }, + }); + console.log("here", organization); + if (!organization) { + throw new ResourceNotFound( + `Organization with ID ${organizationId} not found`, + ); + } + + const invites = emails.map((email) => { + const token = uuidv4(); + return inviteRepository.create({ + token, + email: email, + isGeneric: false, + organization: { id: organizationId }, + }); + }); + + invites.forEach((invite) => { + const inviteLink = `${frontendBaseUrl}/invite?token=${invite.token}`; + const emailContent = { + userName: invite.email.split("@")[0], + title: "Invitation to Join Organization", + body: `

You have been invited to join ${organization.name} organization. Please use the following link to accept the invitation:

Here`, + }; + + const mailOptions = { + from: "admin@mail.com", + to: invite.email, + subject: "Invitation to Join Organization", + html: renderTemplate("custom-email", emailContent), + }; + + addEmailToQueue(mailOptions); + }); + await inviteRepository.save(invites); + } + + async addUserToOrganizationWithInvite( + token: string, + userId: string, + ): Promise { + const inviteRepository = AppDataSource.getRepository(Invitation); + const userOrganizationRepository = + AppDataSource.getRepository(UserOrganization); + const userRepository = AppDataSource.getRepository(User); + const invite = await inviteRepository.findOne({ + where: { token }, + relations: ["organization"], + }); + + if (!invite) { + throw new ResourceNotFound("Invalid or expired invite token"); + } + + const user = await userRepository.findOne({ where: { id: userId } }); + if (!user) { + throw new ResourceNotFound("Please register to join the organization."); + } + const existingMembership = await userOrganizationRepository.findOne({ + where: { + userId: user.id, + organizationId: invite.organization.id, + }, + }); + + if (existingMembership) { + throw new Conflict("User already added to organization."); + } + + const userOrganization = userOrganizationRepository.create({ + userId: user.id, + organizationId: invite.organization.id, + user: user, + organization: invite.organization, + role: user.role, + }); + await userOrganizationRepository.save(userOrganization); + + invite.isAccepted = true; + await inviteRepository.save(invite); + + return "User added to organization successfully"; + } + async getAllInvite( + page: number, + pageSize: number, + ): Promise<{ + message: string; + data: Partial[]; + total: number; + status_code: number; + }> { + const inviteRepository = AppDataSource.getRepository(Invitation); + + const [invites, total] = await inviteRepository.findAndCount({ + skip: (page - 1) * pageSize, + take: pageSize, + }); + + if (invites.length === 0) { + return { + status_code: 200, + message: "No invites yet", + data: invites, + total, + }; + } + + const sentInvites = invites.map((invite) => { + return { + id: invite.id, + token: invite.token, + isAccepted: invite.isAccepted, + isGeneric: invite.isGeneric, + organization: invite.organization, + email: invite.email, + }; + }); + + return { + status_code: 200, + message: "Successfully fetched invites", + data: sentInvites, + total, + }; + } + + public async searchOrganizationMembers(criteria: { + name?: string; + email?: string; + }): Promise { + const userOrganizationRepository = + AppDataSource.getRepository(UserOrganization); + + const { name, email } = criteria; + + const query = userOrganizationRepository + .createQueryBuilder("userOrganization") + .leftJoinAndSelect("userOrganization.user", "user") + .leftJoinAndSelect("userOrganization.organization", "organization") + .where("1=1"); + + if (name) { + query.andWhere("LOWER(user.name) LIKE LOWER(:name)", { + name: `%${name}%`, + }); + } else if (email) { + query.andWhere("LOWER(user.email) LIKE LOWER(:email)", { + email: `%${email}%`, + }); + } + + const userOrganizations = await query.getMany(); + + if (userOrganizations.length > 0) { + const organizationsMap = new Map(); + + userOrganizations.forEach((userOrg) => { + const org = userOrg.organization; + const user = userOrg.user; + + if (!organizationsMap.has(org.id)) { + organizationsMap.set(org.id, { + organizationId: org.id, + organizationName: org.name, + organizationEmail: org.email, + members: [], + }); + } + + organizationsMap.get(org.id).members.push({ + userId: user.id, + userName: user.name, + userEmail: user.email, + }); + }); + + return Array.from(organizationsMap.values()); + } + + return []; + } + + public async createOrganizationRole( + payload: ICreateOrgRole, + organizationid: string, + ) { + try { + const organization = await this.organizationRepository.findOne({ + where: { id: organizationid }, + }); + + if (!organization) { + throw new ResourceNotFound("Organization not found"); + } + + const existingRole = await this.organizationRoleRepository.findOne({ + where: { name: payload.name, organization: { id: organizationid } }, + }); + + if (existingRole) { + throw new Conflict("Role already exists"); + } + + const role = new OrganizationRole(); + Object.assign(role, { + name: payload.name, + description: payload.description, + organization: organization, + }); + const newRole = await this.organizationRoleRepository.save(role); + + const defaultPermissions = await this.permissionRepository.find(); + + const rolePermissions = defaultPermissions.map((defaultPerm) => { + const permission = new Permissions(); + permission.category = defaultPerm.category; + permission.permission_list = defaultPerm.permission_list; + permission.role = newRole; + return permission; + }); + + await this.permissionRepository.save(rolePermissions); + return newRole; + } catch (err) { + throw err; + } + } + public async fetchSingleRole( + organizationId: string, + roleId: string, + ): Promise { + try { + const organisation = await this.organizationRepository.findOne({ + where: { id: organizationId }, + }); + if (!organisation) { + throw new ResourceNotFound( + `Organisation with ID ${organizationId} not found`, + ); + } + + const role = await this.organizationRoleRepository.findOne({ + where: { id: roleId, organization: { id: organizationId } }, + relations: ["permissions"], + }); + if (!role) { + return null; + } + + return role; + } catch (error) { + throw error; + } + } + + public async fetchAllRolesInOrganization(organizationId: string) { + try { + const organization = await this.organizationRepository.findOne({ + where: { id: organizationId }, + }); + + if (!organization) { + throw new ResourceNotFound("Organization not found"); + } + + const roles = await this.organizationRoleRepository.find({ + where: { organization: { id: organizationId } }, + select: ["id", "name", "description"], + }); + + return roles; + } catch (error) { + throw error; + } + } + + public async updateRolePermissions( + roleId: string, + organizationId: string, + newPermissions: PermissionCategory[], + ) { + try { + const organization = await this.organizationRepository.findOne({ + where: { id: organizationId }, + }); + + if (!organization) { + throw new ResourceNotFound("Organization not found"); + } + + const role = await this.organizationRoleRepository.findOne({ + where: { id: roleId, organization: { id: organizationId } }, + relations: ["permissions"], + }); + + if (!role) { + throw new ResourceNotFound("Role not found"); + } + + const newPermissionsSet = new Set(newPermissions); + + role.permissions = role.permissions.filter((permission) => + newPermissionsSet.has(permission.category), + ); + + const existingCategories = new Set( + role.permissions.map((permission) => permission.category), + ); + + for (const category of newPermissions) { + if (!existingCategories.has(category)) { + const newPermission = this.permissionRepository.create({ + category, + role, + permission_list: true, + }); + role.permissions.push(newPermission); + } + } + + role.permissions = role.permissions.filter((permission) => + newPermissionsSet.has(permission.category), + ); + + await this.organizationRoleRepository.save(role); + + return role; + } catch (error) { + throw error; + } + } +} diff --git a/src/services/payment/flutter.service.ts b/src/services/payment/flutter.service.ts index 327740ed..ed8612aa 100644 --- a/src/services/payment/flutter.service.ts +++ b/src/services/payment/flutter.service.ts @@ -1,117 +1,117 @@ -import Flutterwave from "flutterwave-node-v3"; -import { v4 as uuidv4 } from "uuid"; -import config from "../../config"; -import { Payment } from "../../models"; -import AppDataSource from "../../data-source"; - -// Initialize Flutterwave -const flw = new Flutterwave( - config.FLW_PUBLIC_KEY || "mock", - config.FLW_SECRET_KEY || "mock", -); - -interface CustomerDetails { - card_number: string; - cvv: string; - expiry_month: string; - expiry_year: string; - email: string; - fullname: string; - phone_number: string; - currency: string; - amount: number; - payer_id: string; - payer_type: "user" | "organization"; - userId: string; -} - -/** - * Initialize a payment with Flutterwave. - * - * @param {CustomerDetails} customerDetails - The customer's payment details. - * @returns {Promise} - The initialization response from Flutterwave. - */ -export const initializePayment = async ( - customerDetails: CustomerDetails, -): Promise => { - try { - const { userId, ...detailsWithoutUserId } = customerDetails; // Destructure to remove userId - const tx_ref = `flw-${uuidv4()}-${Date.now()}`; - const payload = { - ...detailsWithoutUserId, - tx_ref, - enckey: config.FLW_ENCRYPTION_KEY, - }; - const response = await flw.Charge.card(payload); - - await saveTransactionToDatabase({ - ...customerDetails, - description: `Payment of ${detailsWithoutUserId.amount} ${detailsWithoutUserId.currency} via Flutterwave`, - metadata: { tx_ref, flutterwave_response: response }, - // status: response.data.status, - id: response.data.metadata.data.id, - status: "completed", - provider: "flutterwave", - }); - // console.log(response); - return response; - } catch (error) { - throw error; - } -}; - -/** - * Verify a payment with Flutterwave. - * - * @param {string} transactionId - The transaction ID to verify. - * @returns {Promise} - The verification response from Flutterwave. - */ -export const verifyPayment = async (transactionId: string): Promise => { - try { - const transactionIdNumber = Number(transactionId); - const response = await flw.Transaction.verify({ id: transactionIdNumber }); - - const paymentStatus = - response.data.status === "successful" ? "completed" : "failed"; - await updatePaymentStatus(transactionIdNumber, paymentStatus); - - console.log(response); - return response; - } catch (error) { - console.log(error); - throw error; - } -}; - -/** - * Save transaction details to the database. - * - * @param {Partial} transactionDetails - The details of the transaction to save. - */ -async function saveTransactionToDatabase( - transactionDetails: Partial, -): Promise { - const paymentRepository = AppDataSource.getRepository(Payment); - await paymentRepository.save(transactionDetails); -} - -/** - * Update the payment status in the database. - * - * @param {string} transactionId - The transaction ID to update. - * @param {'completed' | 'failed'} status - The new status of the payment. - */ -async function updatePaymentStatus( - transactionIdNumber: number, - status: "completed" | "failed", -): Promise { - const paymentRepository = AppDataSource.getRepository(Payment); - await paymentRepository - .createQueryBuilder() - .update(Payment) - .set({ status }) - .where(`metadata->'data'->>'id' = :transactionIdNumber`, { - transactionIdNumber, - }) - .execute(); -} +import Flutterwave from "flutterwave-node-v3"; +import { v4 as uuidv4 } from "uuid"; +import config from "../../config"; +import { Payment } from "../../models"; +import AppDataSource from "../../data-source"; + +// Initialize Flutterwave +const flw = new Flutterwave( + config.FLW_PUBLIC_KEY || "mock", + config.FLW_SECRET_KEY || "mock", +); + +interface CustomerDetails { + card_number: string; + cvv: string; + expiry_month: string; + expiry_year: string; + email: string; + fullname: string; + phone_number: string; + currency: string; + amount: number; + payer_id: string; + payer_type: "user" | "organization"; + userId: string; +} + +/** + * Initialize a payment with Flutterwave. + * + * @param {CustomerDetails} customerDetails - The customer's payment details. + * @returns {Promise} - The initialization response from Flutterwave. + */ +export const initializePayment = async ( + customerDetails: CustomerDetails, +): Promise => { + try { + const { userId, ...detailsWithoutUserId } = customerDetails; // Destructure to remove userId + const tx_ref = `flw-${uuidv4()}-${Date.now()}`; + const payload = { + ...detailsWithoutUserId, + tx_ref, + enckey: config.FLW_ENCRYPTION_KEY, + }; + const response = await flw.Charge.card(payload); + + await saveTransactionToDatabase({ + ...customerDetails, + description: `Payment of ${detailsWithoutUserId.amount} ${detailsWithoutUserId.currency} via Flutterwave`, + metadata: { tx_ref, flutterwave_response: response }, + // status: response.data.status, + id: response.data.metadata.data.id, + status: "completed", + provider: "flutterwave", + }); + // console.log(response); + return response; + } catch (error) { + throw error; + } +}; + +/** + * Verify a payment with Flutterwave. + * + * @param {string} transactionId - The transaction ID to verify. + * @returns {Promise} - The verification response from Flutterwave. + */ +export const verifyPayment = async (transactionId: string): Promise => { + try { + const transactionIdNumber = Number(transactionId); + const response = await flw.Transaction.verify({ id: transactionIdNumber }); + + const paymentStatus = + response.data.status === "successful" ? "completed" : "failed"; + await updatePaymentStatus(transactionIdNumber, paymentStatus); + + console.log(response); + return response; + } catch (error) { + console.log(error); + throw error; + } +}; + +/** + * Save transaction details to the database. + * + * @param {Partial} transactionDetails - The details of the transaction to save. + */ +async function saveTransactionToDatabase( + transactionDetails: Partial, +): Promise { + const paymentRepository = AppDataSource.getRepository(Payment); + await paymentRepository.save(transactionDetails); +} + +/** + * Update the payment status in the database. + * + * @param {string} transactionId - The transaction ID to update. + * @param {'completed' | 'failed'} status - The new status of the payment. + */ +async function updatePaymentStatus( + transactionIdNumber: number, + status: "completed" | "failed", +): Promise { + const paymentRepository = AppDataSource.getRepository(Payment); + await paymentRepository + .createQueryBuilder() + .update(Payment) + .set({ status }) + .where(`metadata->'data'->>'id' = :transactionIdNumber`, { + transactionIdNumber, + }) + .execute(); +} diff --git a/src/services/product.services.ts b/src/services/product.services.ts index daffacd0..db79d8f9 100644 --- a/src/services/product.services.ts +++ b/src/services/product.services.ts @@ -1,201 +1,218 @@ -import { Repository } from "typeorm"; -import AppDataSource from "../data-source"; -import { StockStatus } from "../enums/product"; -import { - HttpError, - InvalidInput, - ResourceNotFound, - ServerError, -} from "../middleware"; -import { Organization } from "../models/organization"; -import { Product } from "../models/product"; -import { ProductSchema } from "../schema/product.schema"; - -export class ProductService { - private productRepository: Repository; - private organizationRepository: Repository; - - private entities: { - [key: string]: { - repo: Repository; - name: string; - }; - }; - - constructor() { - this.productRepository = AppDataSource.getRepository(Product); - this.organizationRepository = AppDataSource.getRepository(Organization); - - this.entities = { - product: { - repo: this.productRepository, - name: "Product", - }, - organization: { - repo: this.organizationRepository, - name: "Organization", - }, - }; - } - - private async checkEntities( - entitiesToCheck: { - [key: string]: string; - } = { product: "" }, - ): Promise<{ [key: string]: any }> { - const foundEntities: { [key: string]: any } = {}; - - for (const [entityKey, id] of Object.entries(entitiesToCheck)) { - const entity = this.entities[entityKey]; - if (!entity) { - throw new InvalidInput(`Invalid entity type: ${entityKey}`); - } - - if (!id) { - throw new InvalidInput(`${entity.name} ID not provided`); - } - - const found = await entity.repo.findOne({ where: { id } }); - if (!found) { - throw new ResourceNotFound(`${entity.name} with id ${id} not found`); - } - - foundEntities[entityKey] = found; - } - - return foundEntities; - } - - private async calculateProductStatus(quantity: number): Promise { - if (quantity === 0) { - return StockStatus.OUT_STOCK; - } - return quantity >= 5 ? StockStatus.IN_STOCK : StockStatus.LOW_STOCK; - } - - public async createProduct(orgId: string, new_Product: ProductSchema) { - const { organization } = await this.checkEntities({ organization: orgId }); - if (!organization) { - throw new ServerError("Invalid organisation credentials"); - } - - const newProduct = this.productRepository.create(new_Product); - newProduct.org = organization; - newProduct.stock_status = await this.calculateProductStatus( - new_Product.quantity ?? 0, - ); - - const product = await this.productRepository.save(newProduct); - if (!product) { - throw new ServerError( - "An unexpected error occurred. Please try again later.", - ); - } - - return { - status_code: 201, - status: "success", - message: "Product created successfully", - data: { - id: product.id, - name: product.name, - description: product.description, - price: product.price, - image: product.image, - status: product.stock_status, - quantity: product.quantity, - created_at: product.created_at, - updated_at: product.updated_at, - }, - }; - } - - public async getProducts( - orgId: string, - query: { - name?: string; - category?: string; - minPrice?: number; - maxPrice?: number; - }, - page: number = 1, - limit: number = 10, - ) { - const org = await this.organizationRepository.findOne({ - where: { id: orgId }, - }); - if (!org) { - throw new ServerError( - "Unprocessable entity exception: Invalid organization credentials", - ); - } - - const { name, category, minPrice, maxPrice } = query; - const queryBuilder = this.productRepository - .createQueryBuilder("product") - .where("product.orgId = :orgId", { orgId }); - - if (name) { - queryBuilder.andWhere("product.name ILIKE :name", { name: `%${name}%` }); - } - if (minPrice) { - queryBuilder.andWhere("product.price >= :minPrice", { minPrice }); - } - if (maxPrice) { - queryBuilder.andWhere("product.price <= :maxPrice", { maxPrice }); - } - - const skip = (page - 1) * limit; - queryBuilder.skip(skip).take(limit); - - const [products, total] = await queryBuilder.getManyAndCount(); - - return { - success: true, - statusCode: 200, - data: { - products, - pagination: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }, - }; - } - - public async deleteProduct(org_id: string, product_id: string) { - try { - const entities = await this.checkEntities({ - organization: org_id, - product: product_id, - }); - - if (!entities.product) { - throw new Error("Product not found"); - } - - await this.productRepository.remove(entities.product); - return { message: "Product deleted successfully" }; - } catch (error) { - throw new Error(`Failed to delete product: ${error.message}`); - } - } - - public async getProduct(org_id: string, product_id: string) { - try { - const entities = await this.checkEntities({ - organization: org_id, - product: product_id, - }); - - if (!entities.product) { - return new HttpError(404, "Product not found"); - } - return entities.product; - } catch (error) { - throw new ResourceNotFound(error.message); - } - } -} +import { Repository } from "typeorm"; +import { Product } from "../models/product"; +import AppDataSource from "../data-source"; +import { ProductSize, StockStatus } from "../enums/product"; +import { ProductSchema } from "../schema/product.schema"; +import { InvalidInput, ResourceNotFound, ServerError, HttpError } from "../middleware"; +import { Organization } from "../models/organization"; +import { UserRole } from "../enums/userRoles"; + +export class ProductService { + private productRepository: Repository; + private organizationRepository: Repository; + + private entities: { + [key: string]: { + repo: Repository; + name: string; + }; + }; + + constructor() { + this.productRepository = AppDataSource.getRepository(Product); + this.organizationRepository = AppDataSource.getRepository(Organization); + + this.entities = { + product: { + repo: this.productRepository, + name: "Product", + }, + organization: { + repo: this.organizationRepository, + name: "Organization", + }, + }; + } + + private async checkEntities( + entitiesToCheck: { + [key: string]: string; + } = { product: "" }, + ): Promise<{ [key: string]: any }> { + const foundEntities: { [key: string]: any } = {}; + + for (const [entityKey, id] of Object.entries(entitiesToCheck)) { + const entity = this.entities[entityKey]; + if (!entity) { + throw new InvalidInput(`Invalid entity type: ${entityKey}`); + } + + if (!id) { + throw new InvalidInput(`${entity.name} ID not provided`); + } + + const found = await entity.repo.findOne({ where: { id } }); + if (!found) { + throw new ResourceNotFound(`${entity.name} with id ${id} not found`); + } + + foundEntities[entityKey] = found; + } + + return foundEntities; + } + + private async calculateProductStatus(quantity: number): Promise { + if (quantity === 0) { + return StockStatus.OUT_STOCK; + } + return quantity >= 5 ? StockStatus.IN_STOCK : StockStatus.LOW_STOCK; + } + + public async createProduct(orgId: string, new_Product: ProductSchema) { + const { organization } = await this.checkEntities({ organization: orgId }); + if (!organization) { + throw new ServerError("Invalid organisation credentials"); + } + + const newProduct = this.productRepository.create(new_Product); + newProduct.org = organization; + newProduct.stock_status = await this.calculateProductStatus( + new_Product.quantity ?? 0, + ); + + const product = await this.productRepository.save(newProduct); + if (!product) { + throw new ServerError( + "An unexpected error occurred. Please try again later.", + ); + } + + return { + status_code: 201, + status: "success", + message: "Product created successfully", + data: { + id: product.id, + name: product.name, + description: product.description, + price: product.price, + image: product.image, + status: product.stock_status, + quantity: product.quantity, + created_at: product.created_at, + updated_at: product.updated_at, + }, + }; + } + + public async getProducts( + orgId: string, + query: { + name?: string; + category?: string; + minPrice?: number; + maxPrice?: number; + }, + page: number = 1, + limit: number = 10, + ) { + const org = await this.organizationRepository.findOne({ + where: { id: orgId }, + }); + if (!org) { + throw new ServerError( + "Unprocessable entity exception: Invalid organization credentials", + ); + } + + const { name, category, minPrice, maxPrice } = query; + const queryBuilder = this.productRepository + .createQueryBuilder("product") + .where("product.orgId = :orgId", { orgId }); + + if (name) { + queryBuilder.andWhere("product.name ILIKE :name", { name: `%${name}%` }); + } + if (minPrice) { + queryBuilder.andWhere("product.price >= :minPrice", { minPrice }); + } + if (maxPrice) { + queryBuilder.andWhere("product.price <= :maxPrice", { maxPrice }); + } + + const skip = (page - 1) * limit; + queryBuilder.skip(skip).take(limit); + + const [products, total] = await queryBuilder.getManyAndCount(); + + return { + success: true, + statusCode: 200, + data: { + products, + pagination: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }, + }; + } + + public async deleteProduct(org_id: string, product_id: string) { + try { + const entities = await this.checkEntities({ + organization: org_id, + product: product_id, + }); + + if (!entities.product) { + throw new Error("Product not found"); + } + + await this.productRepository.remove(entities.product); + return { message: "Product deleted successfully" }; + } catch (error) { + throw new Error(`Failed to delete product: ${error.message}`); + } + } + + public async updateProduct( + org_id: string, + product_id: string, + productDetails, + ) { + const entities = await this.checkEntities({ + organization: org_id, + product: product_id, + }); + + const updatedProduct = await this.productRepository.save({ + ...entities.product, + ...productDetails, + }); + if (!updatedProduct) { + throw new ServerError("Internal server Error"); + } + return updatedProduct; + } + + async getProduct(org_id: string, product_id: string) { + try { + const entities = await this.checkEntities({ + organization: org_id, + product: product_id, + }); + + if (!entities.product) { + return new HttpError(404, "Product not found"); + } + return entities.product; + } catch (error) { + throw new ResourceNotFound(error.message); + } + } + +} diff --git a/src/services/role.services.ts b/src/services/role.services.ts index 2423bf06..f45f4c8c 100644 --- a/src/services/role.services.ts +++ b/src/services/role.services.ts @@ -1,27 +1,27 @@ -import { User } from "../models"; -import { UserRole } from "../enums/userRoles"; -import { HttpError } from "../middleware/error"; - -export const createRole = async ( - userId: string, - newRole: UserRole, -): Promise => { - try { - const user = await User.findOne({ where: { id: userId } }); - - if (!user) { - throw new HttpError(404, "User not found"); - } - - if (!Object.values(UserRole).includes(newRole)) { - throw new HttpError(400, "Invalid role specified"); - } - - user.role = newRole; - await user.save(); - - return user; - } catch (error) { - throw error; - } -}; +import { User } from "../models"; +import { UserRole } from "../enums/userRoles"; +import { HttpError } from "../middleware/error"; + +export const createRole = async ( + userId: string, + newRole: UserRole, +): Promise => { + try { + const user = await User.findOne({ where: { id: userId } }); + + if (!user) { + throw new HttpError(404, "User not found"); + } + + if (!Object.values(UserRole).includes(newRole)) { + throw new HttpError(400, "Invalid role specified"); + } + + user.role = newRole; + await user.save(); + + return user; + } catch (error) { + throw error; + } +}; diff --git a/src/services/sendEmail.services.ts b/src/services/sendEmail.services.ts index ef12dc8b..efda4882 100644 --- a/src/services/sendEmail.services.ts +++ b/src/services/sendEmail.services.ts @@ -1,107 +1,107 @@ -import AppDataSource from "../data-source"; -import { EmailQueue, User } from "../models"; -import { EmailQueuePayload } from "../types"; -import { addEmailToQueue } from "../utils/queue"; -import config from "../config"; -import { ServerError } from "../middleware"; -import Handlebars from "handlebars"; -import path from "path"; -import fs from "fs"; -import renderTemplate from "../views/email/renderTemplate"; - -export class EmailService { - async getEmailTemplates(): Promise<{}[]> { - const templateDir = path.resolve("src/views/email/templates"); - const templates = fs.readdirSync(templateDir); - const availableTemplate = templates.map((template) => { - return { templateId: template.split(".")[0] }; - }); - - return availableTemplate; - } - - async queueEmail( - payload: EmailQueuePayload, - user: User, - ): Promise { - const emailQueueRepository = AppDataSource.getRepository(EmailQueue); - const newEmail = emailQueueRepository.create(payload); - await emailQueueRepository.save(newEmail); - - const templatePath = path.resolve( - `src/views/email/templates/${payload.templateId}.hbs`, - ); - if (!fs.existsSync(templatePath)) { - throw new ServerError("Invalid template id" + templatePath); - } - - const data = { - title: payload.variables?.title, - logoUrl: payload.variables?.logoUrl || "https://example.com/logo.png", - imageUrl: - payload.variables?.imageUrl || - "https://exampleImg.com/reset-password.png", - userName: payload.variables?.userName || "User", - activationLinkUrl: payload.variables?.activationLink, - resetUrl: payload.variables?.resetUrl, - body: payload.variables?.body, - companyName: payload.variables?.companyName || "Boilerplate", - supportUrl: - payload.variables?.supportUrl || "https://example.com/support", - socialIcons: payload.variables?.socialIcons || [ - { - url: "https://facebook.com", - imgSrc: - "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png", - alt: "Facebook", - }, - { - url: "https://twitter.com", - imgSrc: - "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png", - alt: "Twitter", - }, - { - url: "https://instagram.com", - imgSrc: - "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png", - alt: "Instagram", - }, - ], - companyWebsite: - payload.variables?.companyWebsite || "https://example.com", - preferencesUrl: - payload.variables?.preferencesUrl || "https://example.com/preferences", - unsubscribeUrl: - payload.variables?.unsubscribeUrl || "https://example.com/unsubscribe", - }; - - const templateSource = fs.readFileSync(templatePath, "utf8"); - const template = Handlebars.compile(templateSource); - const htmlTemplate = template(data); - - const varibles = { - userName: payload.variables?.userName || "User", - title: payload.variables?.title, - // activationLinkUrl:"https://example.com" - }; - - const emailContent = { - from: config.SMTP_USER, - to: payload.recipient, - subject: data.title, - html: renderTemplate(payload.templateId, varibles), - }; - - await addEmailToQueue(emailContent); - - return newEmail; - } - - // async sendEmail(payload: EmailQueuePayload): Promise { - // try { - // } catch (error) { - // throw new ServerError( "Internal server error"); - // } - // } -} +import AppDataSource from "../data-source"; +import { EmailQueue, User } from "../models"; +import { EmailQueuePayload } from "../types"; +import { addEmailToQueue } from "../utils/queue"; +import config from "../config"; +import { ServerError } from "../middleware"; +import Handlebars from "handlebars"; +import path from "path"; +import fs from "fs"; +import renderTemplate from "../views/email/renderTemplate"; + +export class EmailService { + async getEmailTemplates(): Promise<{}[]> { + const templateDir = path.resolve("src/views/email/templates"); + const templates = fs.readdirSync(templateDir); + const availableTemplate = templates.map((template) => { + return { templateId: template.split(".")[0] }; + }); + + return availableTemplate; + } + + async queueEmail( + payload: EmailQueuePayload, + user: User, + ): Promise { + const emailQueueRepository = AppDataSource.getRepository(EmailQueue); + const newEmail = emailQueueRepository.create(payload); + await emailQueueRepository.save(newEmail); + + const templatePath = path.resolve( + `src/views/email/templates/${payload.templateId}.hbs`, + ); + if (!fs.existsSync(templatePath)) { + throw new ServerError("Invalid template id" + templatePath); + } + + const data = { + title: payload.variables?.title, + logoUrl: payload.variables?.logoUrl || "https://example.com/logo.png", + imageUrl: + payload.variables?.imageUrl || + "https://exampleImg.com/reset-password.png", + userName: payload.variables?.userName || "User", + activationLinkUrl: payload.variables?.activationLink, + resetUrl: payload.variables?.resetUrl, + body: payload.variables?.body, + companyName: payload.variables?.companyName || "Boilerplate", + supportUrl: + payload.variables?.supportUrl || "https://example.com/support", + socialIcons: payload.variables?.socialIcons || [ + { + url: "https://facebook.com", + imgSrc: + "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png", + alt: "Facebook", + }, + { + url: "https://twitter.com", + imgSrc: + "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png", + alt: "Twitter", + }, + { + url: "https://instagram.com", + imgSrc: + "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png", + alt: "Instagram", + }, + ], + companyWebsite: + payload.variables?.companyWebsite || "https://example.com", + preferencesUrl: + payload.variables?.preferencesUrl || "https://example.com/preferences", + unsubscribeUrl: + payload.variables?.unsubscribeUrl || "https://example.com/unsubscribe", + }; + + const templateSource = fs.readFileSync(templatePath, "utf8"); + const template = Handlebars.compile(templateSource); + const htmlTemplate = template(data); + + const varibles = { + userName: payload.variables?.userName || "User", + title: payload.variables?.title, + // activationLinkUrl:"https://example.com" + }; + + const emailContent = { + from: config.SMTP_USER, + to: payload.recipient, + subject: data.title, + html: renderTemplate(payload.templateId, varibles), + }; + + await addEmailToQueue(emailContent); + + return newEmail; + } + + // async sendEmail(payload: EmailQueuePayload): Promise { + // try { + // } catch (error) { + // throw new ServerError( "Internal server error"); + // } + // } +} diff --git a/src/services/sms.services.ts b/src/services/sms.services.ts index cd1bf979..4b92082d 100644 --- a/src/services/sms.services.ts +++ b/src/services/sms.services.ts @@ -1,31 +1,31 @@ -import Twilio from "twilio"; -import config from "../config"; -import AppDataSource from "../data-source"; -import { Sms } from "../models/sms"; -import { User } from "../models"; - -class SmsService { - private twilioClient: Twilio.Twilio; - constructor() { - this.twilioClient = Twilio(config.TWILIO_SID, config.TWILIO_AUTH_TOKEN); - } - public async sendSms( - sender: User, - phoneNumber: string, - message: string, - ): Promise { - await this.twilioClient.messages.create({ - body: message, - from: config.TWILIO_PHONE_NUMBER, - to: phoneNumber, - }); - const sms = new Sms(); - sms.sender = sender; - sms.phone_number = phoneNumber; - sms.message = message; - const smsRepository = AppDataSource.getRepository(Sms); - await smsRepository.save(sms); - } -} - -export default new SmsService(); +import Twilio from "twilio"; +import config from "../config"; +import AppDataSource from "../data-source"; +import { Sms } from "../models/sms"; +import { User } from "../models"; + +class SmsService { + private twilioClient: Twilio.Twilio; + constructor() { + this.twilioClient = Twilio(config.TWILIO_SID, config.TWILIO_AUTH_TOKEN); + } + public async sendSms( + sender: User, + phoneNumber: string, + message: string, + ): Promise { + await this.twilioClient.messages.create({ + body: message, + from: config.TWILIO_PHONE_NUMBER, + to: phoneNumber, + }); + const sms = new Sms(); + sms.sender = sender; + sms.phone_number = phoneNumber; + sms.message = message; + const smsRepository = AppDataSource.getRepository(Sms); + await smsRepository.save(sms); + } +} + +export default new SmsService(); diff --git a/src/services/updateBlog.services.ts b/src/services/updateBlog.services.ts new file mode 100644 index 00000000..7d3b98f3 --- /dev/null +++ b/src/services/updateBlog.services.ts @@ -0,0 +1,40 @@ +import { Blog } from "../models/blog"; +import AppDataSource from "../data-source"; + +export const updateBlogPost = async ( + id: string, + title: string, + content: string, + published_at?: Date, + image_url?: string, +) => { + const blogRepository = AppDataSource.getRepository(Blog); + + let blog; + try { + blog = await blogRepository.findOne({ where: { id } }); + } catch (error) { + throw new Error("Error finding blog post."); + } + + if (!blog) { + throw new Error("Blog post not found."); + } + + blog.title = title; + blog.content = content; + + if (published_at) { + blog.published_at = published_at; + } + + if (image_url) { + blog.image_url = image_url; + } + + try { + await blogRepository.save(blog); + } catch (error) {} + + return blog; +}; diff --git a/src/swaggerConfig.ts b/src/swaggerConfig.ts index 35061141..76655bd0 100644 --- a/src/swaggerConfig.ts +++ b/src/swaggerConfig.ts @@ -1,51 +1,51 @@ -import swaggerJsdoc, { SwaggerDefinition } from "swagger-jsdoc"; -import { version } from "../package.json"; -import config from "./config"; - -const swaggerDefinition: SwaggerDefinition = { - openapi: "3.1.0", - info: { - title: "BoilerPlate Express API with Swagger", - version: version, - // description: - // "This is a simple CRUD API application made with Express and documented with Swagger", - }, - servers: [ - { - url: `http://localhost:${config.port}/`, - description: "Local server", - }, - { - url: "https://staging.api-expressjs.boilerplate.hng.tech/", - description: "Live server", - }, - ], - components: { - securitySchemes: { - bearerAuth: { - type: "http", - scheme: "bearer", - bearerFormat: "JWT", - }, - }, - }, - security: [ - { - bearerAuth: [], - }, - ], -}; - -const options = { - swaggerDefinition, - apis: [ - "./src/routes/*.ts", - "./src/controllers/*.ts", - "./src/services/*.ts", - "./src/schema/*.ts", - ], // Adjust these paths -}; - -const specs = swaggerJsdoc(options); - -export default specs; +import swaggerJsdoc, { SwaggerDefinition } from "swagger-jsdoc"; +import { version } from "../package.json"; +import config from "./config"; + +const swaggerDefinition: SwaggerDefinition = { + openapi: "3.1.0", + info: { + title: "BoilerPlate Express API with Swagger", + version: version, + // description: + // "This is a simple CRUD API application made with Express and documented with Swagger", + }, + servers: [ + { + url: `http://localhost:${config.port}/`, + description: "Local server", + }, + { + url: "https://staging.api-expressjs.boilerplate.hng.tech/", + description: "Live server", + }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + security: [ + { + bearerAuth: [], + }, + ], +}; + +const options = { + swaggerDefinition, + apis: [ + "./src/routes/*.ts", + "./src/controllers/*.ts", + "./src/services/*.ts", + "./src/schema/*.ts", + ], // Adjust these paths +}; + +const specs = swaggerJsdoc(options); + +export default specs; diff --git a/src/test/billing.spec.ts b/src/test/billing.spec.ts index c57f2b5c..548fa106 100644 --- a/src/test/billing.spec.ts +++ b/src/test/billing.spec.ts @@ -1,73 +1,73 @@ -import { BillingService } from "../services/billing-plans.services"; -import { BillingPlan } from "../models/billing-plan"; -import AppDataSource from "../data-source"; -import { Repository } from "typeorm"; - -jest.mock("../data-source", () => ({ - getRepository: jest.fn(), -})); - -describe("BillingService", () => { - let billingService: BillingService; - let billingRepository: Repository; - - beforeEach(() => { - billingRepository = { - find: jest.fn(), - } as unknown as Repository; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue( - billingRepository, - ); - billingService = new BillingService(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("getAllBillingPlans", () => { - it("should return an array of billing plans", async () => { - // Arrange - const mockBillingPlans: BillingPlan[] = [ - { id: "1", name: "Basic Plan", price: 10 } as BillingPlan, - { id: "2", name: "Premium Plan", price: 20 } as BillingPlan, - ]; - (billingRepository.find as jest.Mock).mockResolvedValue(mockBillingPlans); - - // Act - const result = await billingService.getAllBillingPlans(); - - // Assert - expect(result).toEqual(mockBillingPlans); - expect(billingRepository.find).toHaveBeenCalledTimes(1); - }); - - it("should return an empty array if no billing plans are found", async () => { - // Arrange - const mockBillingPlans: BillingPlan[] = []; - (billingRepository.find as jest.Mock).mockResolvedValue(mockBillingPlans); - - // Act - const result = await billingService.getAllBillingPlans(); - - // Assert - expect(result).toEqual([]); - expect(billingRepository.find).toHaveBeenCalledTimes(1); - }); - - it("should throw an error if the repository fails to fetch billing plans", async () => { - // Arrange - const errorMessage = "Database error"; - (billingRepository.find as jest.Mock).mockRejectedValue( - new Error(errorMessage), - ); - - // Act & Assert - await expect(billingService.getAllBillingPlans()).rejects.toThrow( - "Could not fetch billing plans", - ); - expect(billingRepository.find).toHaveBeenCalledTimes(1); - }); - }); -}); +import { BillingService } from "../services/billing-plans.services"; +import { BillingPlan } from "../models/billing-plan"; +import AppDataSource from "../data-source"; +import { Repository } from "typeorm"; + +jest.mock("../data-source", () => ({ + getRepository: jest.fn(), +})); + +describe("BillingService", () => { + let billingService: BillingService; + let billingRepository: Repository; + + beforeEach(() => { + billingRepository = { + find: jest.fn(), + } as unknown as Repository; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue( + billingRepository, + ); + billingService = new BillingService(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("getAllBillingPlans", () => { + it("should return an array of billing plans", async () => { + // Arrange + const mockBillingPlans: BillingPlan[] = [ + { id: "1", name: "Basic Plan", price: 10 } as BillingPlan, + { id: "2", name: "Premium Plan", price: 20 } as BillingPlan, + ]; + (billingRepository.find as jest.Mock).mockResolvedValue(mockBillingPlans); + + // Act + const result = await billingService.getAllBillingPlans(); + + // Assert + expect(result).toEqual(mockBillingPlans); + expect(billingRepository.find).toHaveBeenCalledTimes(1); + }); + + it("should return an empty array if no billing plans are found", async () => { + // Arrange + const mockBillingPlans: BillingPlan[] = []; + (billingRepository.find as jest.Mock).mockResolvedValue(mockBillingPlans); + + // Act + const result = await billingService.getAllBillingPlans(); + + // Assert + expect(result).toEqual([]); + expect(billingRepository.find).toHaveBeenCalledTimes(1); + }); + + it("should throw an error if the repository fails to fetch billing plans", async () => { + // Arrange + const errorMessage = "Database error"; + (billingRepository.find as jest.Mock).mockRejectedValue( + new Error(errorMessage), + ); + + // Act & Assert + await expect(billingService.getAllBillingPlans()).rejects.toThrow( + "Could not fetch billing plans", + ); + expect(billingRepository.find).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/test/billingPlan.spec.ts b/src/test/billingPlan.spec.ts index 015ae211..ab4125d4 100644 --- a/src/test/billingPlan.spec.ts +++ b/src/test/billingPlan.spec.ts @@ -1,61 +1,61 @@ -import { BillingPlanService } from "../services/billingplan.services"; -import { BillingPlan } from "../models/billing-plan"; -import { ResourceNotFound } from "../middleware"; -import AppDataSource from "../data-source"; -import { Repository } from "typeorm"; -import { Organization } from "../models"; - -describe("BillingPlanService", () => { - let billingPlanService: BillingPlanService; - let mockRepository: jest.Mocked>; - - beforeEach(() => { - mockRepository = { - findOne: jest.fn(), - } as any; - - jest.spyOn(AppDataSource, "getRepository").mockReturnValue(mockRepository); - - billingPlanService = new BillingPlanService(); - }); - - describe("Get a single billing plan", () => { - it("should return a billing plan when given a valid ID", async () => { - const mockBillingPlan: BillingPlan = { - id: "6b792203-dc65-475c-8733-2d018b9e3c7c", - name: "Test Plan", - price: 100, - currency: "USD", - duration: "monthly", - features: [], - organizationId: "", - description: "", - organization: new Organization(), - payments: [], - }; - - mockRepository.findOne.mockResolvedValue(mockBillingPlan); - - const result = await billingPlanService.getBillingPlan( - "6b792203-dc65-475c-8733-2d018b9e3c7c", - ); - - expect(result).toEqual(mockBillingPlan); - expect(mockRepository.findOne).toHaveBeenCalledWith({ - where: { id: "6b792203-dc65-475c-8733-2d018b9e3c7c" }, - }); - }); - - it("should throw ResourceNotFound when given an invalid ID", async () => { - mockRepository.findOne.mockResolvedValue(null); - - await expect( - billingPlanService.getBillingPlan("invalid-id"), - ).rejects.toThrow(ResourceNotFound); - - expect(mockRepository.findOne).toHaveBeenCalledWith({ - where: { id: "invalid-id" }, - }); - }); - }); -}); +import { BillingPlanService } from "../services/billingplan.services"; +import { BillingPlan } from "../models/billing-plan"; +import { ResourceNotFound } from "../middleware"; +import AppDataSource from "../data-source"; +import { Repository } from "typeorm"; +import { Organization } from "../models"; + +describe("BillingPlanService", () => { + let billingPlanService: BillingPlanService; + let mockRepository: jest.Mocked>; + + beforeEach(() => { + mockRepository = { + findOne: jest.fn(), + } as any; + + jest.spyOn(AppDataSource, "getRepository").mockReturnValue(mockRepository); + + billingPlanService = new BillingPlanService(); + }); + + describe("Get a single billing plan", () => { + it("should return a billing plan when given a valid ID", async () => { + const mockBillingPlan: BillingPlan = { + id: "6b792203-dc65-475c-8733-2d018b9e3c7c", + name: "Test Plan", + price: 100, + currency: "USD", + duration: "monthly", + features: [], + organizationId: "", + description: "", + organization: new Organization(), + payments: [], + }; + + mockRepository.findOne.mockResolvedValue(mockBillingPlan); + + const result = await billingPlanService.getBillingPlan( + "6b792203-dc65-475c-8733-2d018b9e3c7c", + ); + + expect(result).toEqual(mockBillingPlan); + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: "6b792203-dc65-475c-8733-2d018b9e3c7c" }, + }); + }); + + it("should throw ResourceNotFound when given an invalid ID", async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect( + billingPlanService.getBillingPlan("invalid-id"), + ).rejects.toThrow(ResourceNotFound); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: "invalid-id" }, + }); + }); + }); +}); diff --git a/src/test/blogComment.spec.ts b/src/test/blogComment.spec.ts index e2b523f7..5a7c2623 100644 --- a/src/test/blogComment.spec.ts +++ b/src/test/blogComment.spec.ts @@ -1,70 +1,70 @@ -import { getAllComments } from "../services/blogComment.services"; -import AppDataSource from "../data-source"; -import { ResourceNotFound } from "../middleware"; - -jest.mock("../data-source", () => ({ - __esModule: true, - default: { - getRepository: jest.fn(), - initialize: jest.fn().mockResolvedValue(true), - }, - initializeDataSource: jest.fn().mockResolvedValue(true), -})); - -describe("getAllComments", () => { - let blogRepository; - - beforeEach(async () => { - jest.clearAllMocks(); - await AppDataSource.initialize(); - blogRepository = { - findOne: jest.fn(), - }; - AppDataSource.getRepository = jest.fn().mockReturnValue(blogRepository); - }); - - it("should get all comments for a blog", async () => { - const blogId = "1"; - - const blog = { - id: blogId, - comments: [ - { - id: 1, - content: "Comment 1", - created_at: new Date("2022-01-01T00:00:00.000Z"), - author: { name: "Author 1" }, - }, - { - id: 2, - content: "Comment 2", - created_at: new Date("2022-01-01T00:00:00.000Z"), - author: { name: "Author 2" }, - }, - ], - }; - - blogRepository.findOne.mockResolvedValue(blog); - - const comments = await getAllComments(blogId); - - expect(comments).toHaveLength(2); - expect(comments[0].id).toBe(1); - expect(comments[0].author).toBe("Author 1"); - expect(comments[0].text).toBe("Comment 1"); - expect(comments[0].timestamp).toBe("2022-01-01T00:00:00.000Z"); - - expect(comments[1].id).toBe(2); - expect(comments[1].author).toBe("Author 2"); - expect(comments[1].text).toBe("Comment 2"); - expect(comments[1].timestamp).toBe("2022-01-01T00:00:00.000Z"); - }); - - it("should throw ResourceNotFound if blog is not found", async () => { - const blogId = "1"; - - blogRepository.findOne.mockResolvedValue(null); - - await expect(getAllComments(blogId)).rejects.toThrow(ResourceNotFound); - }); -}); +import { getAllComments } from "../services/blogComment.services"; +import AppDataSource from "../data-source"; +import { ResourceNotFound } from "../middleware"; + +jest.mock("../data-source", () => ({ + __esModule: true, + default: { + getRepository: jest.fn(), + initialize: jest.fn().mockResolvedValue(true), + }, + initializeDataSource: jest.fn().mockResolvedValue(true), +})); + +describe("getAllComments", () => { + let blogRepository; + + beforeEach(async () => { + jest.clearAllMocks(); + await AppDataSource.initialize(); + blogRepository = { + findOne: jest.fn(), + }; + AppDataSource.getRepository = jest.fn().mockReturnValue(blogRepository); + }); + + it("should get all comments for a blog", async () => { + const blogId = "1"; + + const blog = { + id: blogId, + comments: [ + { + id: 1, + content: "Comment 1", + created_at: new Date("2022-01-01T00:00:00.000Z"), + author: { name: "Author 1" }, + }, + { + id: 2, + content: "Comment 2", + created_at: new Date("2022-01-01T00:00:00.000Z"), + author: { name: "Author 2" }, + }, + ], + }; + + blogRepository.findOne.mockResolvedValue(blog); + + const comments = await getAllComments(blogId); + + expect(comments).toHaveLength(2); + expect(comments[0].id).toBe(1); + expect(comments[0].author).toBe("Author 1"); + expect(comments[0].text).toBe("Comment 1"); + expect(comments[0].timestamp).toBe("2022-01-01T00:00:00.000Z"); + + expect(comments[1].id).toBe(2); + expect(comments[1].author).toBe("Author 2"); + expect(comments[1].text).toBe("Comment 2"); + expect(comments[1].timestamp).toBe("2022-01-01T00:00:00.000Z"); + }); + + it("should throw ResourceNotFound if blog is not found", async () => { + const blogId = "1"; + + blogRepository.findOne.mockResolvedValue(null); + + await expect(getAllComments(blogId)).rejects.toThrow(ResourceNotFound); + }); +}); diff --git a/src/test/checkPermissions.spec.ts b/src/test/checkPermissions.spec.ts index ba69dae6..a3a2377c 100644 --- a/src/test/checkPermissions.spec.ts +++ b/src/test/checkPermissions.spec.ts @@ -1,111 +1,111 @@ -//@ts-nocheck -import { Request, Response, NextFunction } from "express"; -import { checkPermissions } from "../middleware/"; -import { UserRole } from "../enums/userRoles"; -import { User } from "../models"; -import AppDataSource from "../data-source"; -import jwt from "jsonwebtoken"; - -jest.mock("../data-source"); - -describe("checkPermissions middleware", () => { - let req: Request & { user?: User }; - let res: Response; - let next: NextFunction; - let userRepositoryMock: any; - - beforeEach(() => { - req = { - headers: {}, - user: undefined, - } as unknown as Request & { user?: User }; - res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - } as unknown as Response; - next = jest.fn(); - - userRepositoryMock = { - findOne: jest.fn(), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue( - userRepositoryMock, - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it("should allow access if user has required role", async () => { - const token = "valid-jwt-token"; - const decodedToken = { userId: "user-id" }; - const mockUser = { id: "user-id", role: UserRole.ADMIN }; - - req.headers.authorization = `Bearer ${token}`; - jest.spyOn(jwt, "decode").mockReturnValue(decodedToken); - userRepositoryMock.findOne.mockResolvedValue(mockUser); - - const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); - await middleware(req, res, next); - - expect(next).toHaveBeenCalled(); - }); - - it("should deny access if user does not have required role", async () => { - const token = "valid-jwt-token"; - const decodedToken = { userId: "user-id" }; - const mockUser = { id: "user-id", role: UserRole.USER }; - - req.headers.authorization = `Bearer ${token}`; - jest.spyOn(jwt, "decode").mockReturnValue(decodedToken); - userRepositoryMock.findOne.mockResolvedValue(mockUser); - - const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); - await middleware(req, res, next); - - expect(res.status).toHaveBeenCalledWith(403); - expect(res.json).toHaveBeenCalledWith({ - status: "error", - message: "Access denied. Not an admin", - }); - expect(next).not.toHaveBeenCalled(); - }); - - it("should deny access if token is invalid", async () => { - const token = "invalid-jwt-token"; - - req.headers.authorization = `Bearer ${token}`; - jest.spyOn(jwt, "decode").mockReturnValue(null); - - const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); - await middleware(req, res, next); - - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ - status: "error", - message: "Access denied. Invalid token", - }); - expect(next).not.toHaveBeenCalled(); - }); - - it("should deny access if user is not found", async () => { - const token = "valid-jwt-token"; - const decodedToken = { userId: "user-id" }; - - req.headers.authorization = `Bearer ${token}`; - jest.spyOn(jwt, "decode").mockReturnValue(decodedToken); - userRepositoryMock.findOne.mockResolvedValue(null); - - const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); - await middleware(req, res, next); - - expect(res.status).toHaveBeenCalledWith(403); - expect(res.json).toHaveBeenCalledWith({ - status: "error", - message: "Access denied. Not an admin", - }); - expect(next).not.toHaveBeenCalled(); - }); -}); +//@ts-nocheck +import { Request, Response, NextFunction } from "express"; +import { checkPermissions } from "../middleware/"; +import { UserRole } from "../enums/userRoles"; +import { User } from "../models"; +import AppDataSource from "../data-source"; +import jwt from "jsonwebtoken"; + +jest.mock("../data-source"); + +describe("checkPermissions middleware", () => { + let req: Request & { user?: User }; + let res: Response; + let next: NextFunction; + let userRepositoryMock: any; + + beforeEach(() => { + req = { + headers: {}, + user: undefined, + } as unknown as Request & { user?: User }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + } as unknown as Response; + next = jest.fn(); + + userRepositoryMock = { + findOne: jest.fn(), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue( + userRepositoryMock, + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should allow access if user has required role", async () => { + const token = "valid-jwt-token"; + const decodedToken = { userId: "user-id" }; + const mockUser = { id: "user-id", role: UserRole.ADMIN }; + + req.headers.authorization = `Bearer ${token}`; + jest.spyOn(jwt, "decode").mockReturnValue(decodedToken); + userRepositoryMock.findOne.mockResolvedValue(mockUser); + + const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + it("should deny access if user does not have required role", async () => { + const token = "valid-jwt-token"; + const decodedToken = { userId: "user-id" }; + const mockUser = { id: "user-id", role: UserRole.USER }; + + req.headers.authorization = `Bearer ${token}`; + jest.spyOn(jwt, "decode").mockReturnValue(decodedToken); + userRepositoryMock.findOne.mockResolvedValue(mockUser); + + const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ + status: "error", + message: "Access denied. Not an admin", + }); + expect(next).not.toHaveBeenCalled(); + }); + + it("should deny access if token is invalid", async () => { + const token = "invalid-jwt-token"; + + req.headers.authorization = `Bearer ${token}`; + jest.spyOn(jwt, "decode").mockReturnValue(null); + + const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ + status: "error", + message: "Access denied. Invalid token", + }); + expect(next).not.toHaveBeenCalled(); + }); + + it("should deny access if user is not found", async () => { + const token = "valid-jwt-token"; + const decodedToken = { userId: "user-id" }; + + req.headers.authorization = `Bearer ${token}`; + jest.spyOn(jwt, "decode").mockReturnValue(decodedToken); + userRepositoryMock.findOne.mockResolvedValue(null); + + const middleware = checkPermissions([UserRole.SUPER_ADMIN, UserRole.ADMIN]); + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ + status: "error", + message: "Access denied. Not an admin", + }); + expect(next).not.toHaveBeenCalled(); + }); +}); diff --git a/src/test/contact.spec.ts b/src/test/contact.spec.ts index 06edaad8..1fe95b17 100644 --- a/src/test/contact.spec.ts +++ b/src/test/contact.spec.ts @@ -1,51 +1,51 @@ -import { validateContact } from "../utils/contactValidator"; - -describe("validateContact", () => { - it("should return no errors for valid contact data", () => { - const validData = { - name: "Korede Akorede", - email: "ibraim@gmail.com", - message: "I am a backend dev", - }; - - const errors = validateContact(validData); - expect(errors).toEqual([]); - }); - - it("should return error for missing name", () => { - const invalidData = { - name: "", - email: "ibraim@gmail.com", - message: "I am a backend dev", - }; - - const errors = validateContact(invalidData); - expect(errors).toContain( - "Please enter your name. It should be less than 100 characters.", - ); - }); - - it("should return error for invalid email format", () => { - const invalidData = { - name: "Korede Akorede", - email: "invalid-email", - message: "I am a backend dev", - }; - - const errors = validateContact(invalidData); - expect(errors).toContain("Please enter a valid email address."); - }); - - it("should return error for message length exceeding 250 characters", () => { - const invalidData = { - name: "Korede Akorede", - email: "ibraim@gmail.com", - message: "a".repeat(251), // Message length exceeds 250 characters - }; - - const errors = validateContact(invalidData); - expect(errors).toContain( - "Please enter your message. It should be less than 250 characters.", - ); - }); -}); +import { validateContact } from "../utils/contactValidator"; + +describe("validateContact", () => { + it("should return no errors for valid contact data", () => { + const validData = { + name: "Korede Akorede", + email: "ibraim@gmail.com", + message: "I am a backend dev", + }; + + const errors = validateContact(validData); + expect(errors).toEqual([]); + }); + + it("should return error for missing name", () => { + const invalidData = { + name: "", + email: "ibraim@gmail.com", + message: "I am a backend dev", + }; + + const errors = validateContact(invalidData); + expect(errors).toContain( + "Please enter your name. It should be less than 100 characters.", + ); + }); + + it("should return error for invalid email format", () => { + const invalidData = { + name: "Korede Akorede", + email: "invalid-email", + message: "I am a backend dev", + }; + + const errors = validateContact(invalidData); + expect(errors).toContain("Please enter a valid email address."); + }); + + it("should return error for message length exceeding 250 characters", () => { + const invalidData = { + name: "Korede Akorede", + email: "ibraim@gmail.com", + message: "a".repeat(251), // Message length exceeds 250 characters + }; + + const errors = validateContact(invalidData); + expect(errors).toContain( + "Please enter your message. It should be less than 250 characters.", + ); + }); +}); diff --git a/src/test/deleteUserFromOrg.spec.ts b/src/test/deleteUserFromOrg.spec.ts index 12c4977f..a2232db7 100644 --- a/src/test/deleteUserFromOrg.spec.ts +++ b/src/test/deleteUserFromOrg.spec.ts @@ -1,90 +1,90 @@ -//@ts-nocheck -import { Request, Response } from "express"; -import { OrgController } from "../controllers"; -import { OrgService } from "../services"; -import { Organization, UserOrganization, User } from "../models"; -import AppDataSource from "../data-source"; - -jest.mock("../data-source"); - -describe("OrgService", () => { - let orgService: OrgService; - - beforeAll(() => { - orgService = new OrgService(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - // it('should remove user from organization', async () => { - // const mockUser = { id: 'user_id' } as User; - // const mockUserOrganization = { - // userId: 'user_id', - // organizationId: 'org_id', - // user: mockUser, - // } as UserOrganization; - - // const userOrganizationRepository = { - // findOne: jest.fn().mockResolvedValue(mockUserOrganization), - // remove: jest.fn().mockResolvedValue(null), - // }; - // const organizationRepository = { - // findOne: jest.fn().mockResolvedValue(null), - // save: jest.fn().mockResolvedValue(null), - // }; - - // (AppDataSource.getRepository as jest.Mock) - // .mockImplementation((entity) => { - // if (entity === UserOrganization) return userOrganizationRepository; - // if (entity === Organization) return organizationRepository; - // return {}; - // }); - - // const result = await orgService.removeUser('org_id', 'user_id'); - // expect(result).toBe(mockUser); - // expect(userOrganizationRepository.findOne).toHaveBeenCalledWith({ - // where: { userId: 'user_id', organizationId: 'org_id' }, - // relations: ['user', 'organization'], - // }); - // expect(userOrganizationRepository.remove).toHaveBeenCalledWith(mockUserOrganization); - // // expect(organizationRepository.findOne).toHaveBeenCalledWith({ - // // where: { id: 'org_id', owner_id: 'user_id' }, - // // relations: ['users'], - // // }); - // }); - - it("should return null if user organization not found", async () => { - const userOrganizationRepository = { - findOne: jest.fn().mockResolvedValue(null), - }; - - (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { - if (entity === UserOrganization) return userOrganizationRepository; - return {}; - }); - - const result = await orgService.removeUser("org_id", "user_id"); - expect(result).toBeNull(); - expect(userOrganizationRepository.findOne).toHaveBeenCalledWith({ - where: { userId: "user_id", organizationId: "org_id" }, - relations: ["user", "organization"], - }); - }); - - it("should throw an error on failure", async () => { - const userOrganizationRepository = { - findOne: jest.fn().mockRejectedValue(new Error("Database error")), - }; - - (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { - if (entity === UserOrganization) return userOrganizationRepository; - return {}; - }); - - await expect(orgService.removeUser("org_id", "user_id")).rejects.toThrow( - "Failed to remove user from organization", - ); - }); -}); +//@ts-nocheck +import { Request, Response } from "express"; +import { OrgController } from "../controllers"; +import { OrgService } from "../services"; +import { Organization, UserOrganization, User } from "../models"; +import AppDataSource from "../data-source"; + +jest.mock("../data-source"); + +describe("OrgService", () => { + let orgService: OrgService; + + beforeAll(() => { + orgService = new OrgService(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + // it('should remove user from organization', async () => { + // const mockUser = { id: 'user_id' } as User; + // const mockUserOrganization = { + // userId: 'user_id', + // organizationId: 'org_id', + // user: mockUser, + // } as UserOrganization; + + // const userOrganizationRepository = { + // findOne: jest.fn().mockResolvedValue(mockUserOrganization), + // remove: jest.fn().mockResolvedValue(null), + // }; + // const organizationRepository = { + // findOne: jest.fn().mockResolvedValue(null), + // save: jest.fn().mockResolvedValue(null), + // }; + + // (AppDataSource.getRepository as jest.Mock) + // .mockImplementation((entity) => { + // if (entity === UserOrganization) return userOrganizationRepository; + // if (entity === Organization) return organizationRepository; + // return {}; + // }); + + // const result = await orgService.removeUser('org_id', 'user_id'); + // expect(result).toBe(mockUser); + // expect(userOrganizationRepository.findOne).toHaveBeenCalledWith({ + // where: { userId: 'user_id', organizationId: 'org_id' }, + // relations: ['user', 'organization'], + // }); + // expect(userOrganizationRepository.remove).toHaveBeenCalledWith(mockUserOrganization); + // // expect(organizationRepository.findOne).toHaveBeenCalledWith({ + // // where: { id: 'org_id', owner_id: 'user_id' }, + // // relations: ['users'], + // // }); + // }); + + it("should return null if user organization not found", async () => { + const userOrganizationRepository = { + findOne: jest.fn().mockResolvedValue(null), + }; + + (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { + if (entity === UserOrganization) return userOrganizationRepository; + return {}; + }); + + const result = await orgService.removeUser("org_id", "user_id"); + expect(result).toBeNull(); + expect(userOrganizationRepository.findOne).toHaveBeenCalledWith({ + where: { userId: "user_id", organizationId: "org_id" }, + relations: ["user", "organization"], + }); + }); + + it("should throw an error on failure", async () => { + const userOrganizationRepository = { + findOne: jest.fn().mockRejectedValue(new Error("Database error")), + }; + + (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { + if (entity === UserOrganization) return userOrganizationRepository; + return {}; + }); + + await expect(orgService.removeUser("org_id", "user_id")).rejects.toThrow( + "Failed to remove user from organization", + ); + }); +}); diff --git a/src/test/emailService.spec.ts b/src/test/emailService.spec.ts index 713bcbc6..0f3b9aaf 100644 --- a/src/test/emailService.spec.ts +++ b/src/test/emailService.spec.ts @@ -1,137 +1,137 @@ -//@ts-nocheck - -import request from "supertest"; -import { Express } from "express"; -import AppDataSource from "../data-source"; -import { User } from "../models"; -import { EmailService } from "../services"; -import { sendEmailRoute } from "../routes/sendEmail.route"; -import express from "express"; - -jest.mock("../services/sendEmail.services", () => { - return { - EmailService: jest.fn().mockImplementation(() => { - return { - getEmailTemplates: jest - .fn() - .mockResolvedValue([{ templateId: "test_template" }]), - queueEmail: jest.fn().mockResolvedValue({}), - sendEmail: jest.fn().mockResolvedValue({}), - }; - }), - }; -}); - -jest.mock("../data-source", () => { - return { - AppDataSource: { - manager: {}, - initialize: jest.fn().mockResolvedValue(true), - }, - getRepository: jest.fn().mockReturnValue({ - findOne: jest.fn(), - save: jest.fn(), - }), - }; -}); - -jest.mock("../middleware", () => { - return { - authMiddleware: (req, res, next) => next(), - }; -}); - -const app: Express = express(); -app.use(express.json()); -app.use(sendEmailRoute); - -describe("SendEmail Controller", () => { - let mockManager; - - beforeEach(() => { - mockManager = { save: jest.fn() }; - AppDataSource.manager = mockManager; - jest.clearAllMocks(); // Clear all mocks before each test - }); - - // Uncomment the following line when the server is live. - // The current server status may be causing errors, preventing code commits. - // Ensure that the server is running and accessible before proceeding. - - // it("should return 400 if template_id or recipient is missing", async () => { - // const res = await request(app) - // .post("/send-email") - // .send({ template_id: "test_template" }); - - // expect(res.status).toBe(400); - // expect(res.body).toEqual({ - // success: false, - // status_code: 400, - // message: "Invalid input. Template ID and recipient are required.", - // }); - // }); - - it("should return 400 if template_id is not found", async () => { - const res = await request(app).post("/send-email").send({ - template_id: "non_existent_template", - recipient: "test@example.com", - }); - - expect(res.status).toBe(400); - expect(res.body).toEqual({ - success: false, - status_code: 400, - message: "Template not found", - available_templates: ["test_template"], - }); - }); - - // it("should return 404 if user is not found", async () => { - // jest - // .spyOn(AppDataSource.getRepository(User), "findOne") - // .mockResolvedValueOnce(null); - - // const res = await request(app) - // .post("/send-email") - // .send({ - // template_id: "test_template", - // recipient: "nonexistent@example.com", - // }); - - // expect(res.status).toBe(404); - // expect(res.body).toEqual({ - // success: false, - // status_code: 404, - // message: "User not found", - // }); - // }); - - it("should return 202 if email is sent successfully", async () => { - jest - .spyOn(AppDataSource.getRepository(User), "findOne") - .mockResolvedValueOnce({ email: "test@example.com" } as User); - - const res = await request(app).post("/send-email").send({ - template_id: "test_template", - recipient: "test@example.com", - variables: {}, - }); - - expect(res.status).toBe(202); - expect(res.body).toEqual({ - message: "Email sending request accepted and is being processed.", - }); - }); -}); - -describe("GetEmailTemplates Controller", () => { - it("should return 200 with available templates", async () => { - const res = await request(app).get("/email-templates"); - - expect(res.status).toBe(200); - expect(res.body).toEqual({ - message: "Available templates", - templates: [{ templateId: "test_template" }], - }); - }); -}); +//@ts-nocheck + +import request from "supertest"; +import { Express } from "express"; +import AppDataSource from "../data-source"; +import { User } from "../models"; +import { EmailService } from "../services"; +import { sendEmailRoute } from "../routes/sendEmail.route"; +import express from "express"; + +jest.mock("../services/sendEmail.services", () => { + return { + EmailService: jest.fn().mockImplementation(() => { + return { + getEmailTemplates: jest + .fn() + .mockResolvedValue([{ templateId: "test_template" }]), + queueEmail: jest.fn().mockResolvedValue({}), + sendEmail: jest.fn().mockResolvedValue({}), + }; + }), + }; +}); + +jest.mock("../data-source", () => { + return { + AppDataSource: { + manager: {}, + initialize: jest.fn().mockResolvedValue(true), + }, + getRepository: jest.fn().mockReturnValue({ + findOne: jest.fn(), + save: jest.fn(), + }), + }; +}); + +jest.mock("../middleware", () => { + return { + authMiddleware: (req, res, next) => next(), + }; +}); + +const app: Express = express(); +app.use(express.json()); +app.use(sendEmailRoute); + +describe("SendEmail Controller", () => { + let mockManager; + + beforeEach(() => { + mockManager = { save: jest.fn() }; + AppDataSource.manager = mockManager; + jest.clearAllMocks(); // Clear all mocks before each test + }); + + // Uncomment the following line when the server is live. + // The current server status may be causing errors, preventing code commits. + // Ensure that the server is running and accessible before proceeding. + + // it("should return 400 if template_id or recipient is missing", async () => { + // const res = await request(app) + // .post("/send-email") + // .send({ template_id: "test_template" }); + + // expect(res.status).toBe(400); + // expect(res.body).toEqual({ + // success: false, + // status_code: 400, + // message: "Invalid input. Template ID and recipient are required.", + // }); + // }); + + it("should return 400 if template_id is not found", async () => { + const res = await request(app).post("/send-email").send({ + template_id: "non_existent_template", + recipient: "test@example.com", + }); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ + success: false, + status_code: 400, + message: "Template not found", + available_templates: ["test_template"], + }); + }); + + // it("should return 404 if user is not found", async () => { + // jest + // .spyOn(AppDataSource.getRepository(User), "findOne") + // .mockResolvedValueOnce(null); + + // const res = await request(app) + // .post("/send-email") + // .send({ + // template_id: "test_template", + // recipient: "nonexistent@example.com", + // }); + + // expect(res.status).toBe(404); + // expect(res.body).toEqual({ + // success: false, + // status_code: 404, + // message: "User not found", + // }); + // }); + + it("should return 202 if email is sent successfully", async () => { + jest + .spyOn(AppDataSource.getRepository(User), "findOne") + .mockResolvedValueOnce({ email: "test@example.com" } as User); + + const res = await request(app).post("/send-email").send({ + template_id: "test_template", + recipient: "test@example.com", + variables: {}, + }); + + expect(res.status).toBe(202); + expect(res.body).toEqual({ + message: "Email sending request accepted and is being processed.", + }); + }); +}); + +describe("GetEmailTemplates Controller", () => { + it("should return 200 with available templates", async () => { + const res = await request(app).get("/email-templates"); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + message: "Available templates", + templates: [{ templateId: "test_template" }], + }); + }); +}); diff --git a/src/test/faq.spec.ts b/src/test/faq.spec.ts index d88e71a2..b3e74a8b 100644 --- a/src/test/faq.spec.ts +++ b/src/test/faq.spec.ts @@ -1,127 +1,127 @@ -import { FAQService } from "../services"; -import { Repository } from "typeorm"; -import { FAQ } from "../models/faq"; -import AppDataSource from "../data-source"; -import { BadRequest, HttpError } from "../middleware"; - -jest.mock("../data-source"); - -describe("FaqService", () => { - let faqService: FAQService; - let faqRepository: jest.Mocked>; - - beforeEach(() => { - faqRepository = { - findOne: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - } as any; - - AppDataSource.getRepository = jest.fn().mockImplementation((model) => { - if (model === FAQ) { - return faqRepository; - } - throw new Error("Unknown model"); - }); - - faqService = new FAQService(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("updateFaq", () => { - it("should update an existing FAQ", async () => { - const faqId = "faq-123"; - const payload = { - question: "Updated FAQ question", - answer: "Updated answer", - category: "General", - }; - const existingFaq = { - id: faqId, - question: "Old FAQ question", - answer: "Old answer", - category: "General", - } as FAQ; - const updatedFaq = { - ...existingFaq, - ...payload, - }; - - faqRepository.findOne.mockResolvedValue(existingFaq); - // faqRepository.update.mockResolvedValue({payload}); - - const result = await faqService.updateFaq(payload, faqId); - - expect(faqRepository.findOne).toHaveBeenCalledWith({ - where: { id: faqId }, - }); - expect(faqRepository.update).toHaveBeenCalledWith(faqId, payload); - expect(faqRepository.findOne).toHaveBeenCalledWith({ - where: { id: faqId }, - }); - expect(result).toEqual(updatedFaq); - }); - - it("should throw BadRequest if FAQ does not exist", async () => { - const faqId = "faq-123"; - const payload = { - question: "Updated FAQ question", - answer: "Updated answer", - category: "General", - }; - - faqRepository.findOne.mockResolvedValue(null); - - await expect(faqService.updateFaq(payload, faqId)).rejects.toThrow( - BadRequest, - ); - }); - }); - - describe("deleteFaq", () => { - it("should delete an existing FAQ", async () => { - const faqId = "faq-123"; - const existingFaq = { - id: faqId, - question: "FAQ question", - answer: "Answer", - category: "General", - } as FAQ; - - faqRepository.findOne.mockResolvedValue(existingFaq); - faqRepository.delete.mockResolvedValue({ affected: 1, raw: [] }); - - const result = await faqService.deleteFaq(faqId); - - expect(faqRepository.findOne).toHaveBeenCalledWith({ - where: { id: faqId }, - }); - expect(faqRepository.delete).toHaveBeenCalledWith(faqId); - expect(result).toBe(true); - }); - - it("should throw BadRequest if FAQ does not exist", async () => { - const faqId = "faq-123"; - - faqRepository.findOne.mockResolvedValue(null); - - await expect(faqService.deleteFaq(faqId)).rejects.toThrow(BadRequest); - }); - - it("should throw HttpError if deletion fails", async () => { - const faqId = "faq-123"; - const existingFaq = { - id: faqId, - question: "FAQ question", - answer: "Answer", - category: "General", - } as FAQ; - - faqRepository.findOne.mockResolvedValue(existingFaq); - faqRepository.delete.mockResolvedValue({ affected: 0, raw: [] }); - }); - }); -}); +import { FAQService } from "../services"; +import { Repository } from "typeorm"; +import { FAQ } from "../models/faq"; +import AppDataSource from "../data-source"; +import { BadRequest, HttpError } from "../middleware"; + +jest.mock("../data-source"); + +describe("FaqService", () => { + let faqService: FAQService; + let faqRepository: jest.Mocked>; + + beforeEach(() => { + faqRepository = { + findOne: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + } as any; + + AppDataSource.getRepository = jest.fn().mockImplementation((model) => { + if (model === FAQ) { + return faqRepository; + } + throw new Error("Unknown model"); + }); + + faqService = new FAQService(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("updateFaq", () => { + it("should update an existing FAQ", async () => { + const faqId = "faq-123"; + const payload = { + question: "Updated FAQ question", + answer: "Updated answer", + category: "General", + }; + const existingFaq = { + id: faqId, + question: "Old FAQ question", + answer: "Old answer", + category: "General", + } as FAQ; + const updatedFaq = { + ...existingFaq, + ...payload, + }; + + faqRepository.findOne.mockResolvedValue(existingFaq); + // faqRepository.update.mockResolvedValue({payload}); + + const result = await faqService.updateFaq(payload, faqId); + + expect(faqRepository.findOne).toHaveBeenCalledWith({ + where: { id: faqId }, + }); + expect(faqRepository.update).toHaveBeenCalledWith(faqId, payload); + expect(faqRepository.findOne).toHaveBeenCalledWith({ + where: { id: faqId }, + }); + expect(result).toEqual(updatedFaq); + }); + + it("should throw BadRequest if FAQ does not exist", async () => { + const faqId = "faq-123"; + const payload = { + question: "Updated FAQ question", + answer: "Updated answer", + category: "General", + }; + + faqRepository.findOne.mockResolvedValue(null); + + await expect(faqService.updateFaq(payload, faqId)).rejects.toThrow( + BadRequest, + ); + }); + }); + + describe("deleteFaq", () => { + it("should delete an existing FAQ", async () => { + const faqId = "faq-123"; + const existingFaq = { + id: faqId, + question: "FAQ question", + answer: "Answer", + category: "General", + } as FAQ; + + faqRepository.findOne.mockResolvedValue(existingFaq); + faqRepository.delete.mockResolvedValue({ affected: 1, raw: [] }); + + const result = await faqService.deleteFaq(faqId); + + expect(faqRepository.findOne).toHaveBeenCalledWith({ + where: { id: faqId }, + }); + expect(faqRepository.delete).toHaveBeenCalledWith(faqId); + expect(result).toBe(true); + }); + + it("should throw BadRequest if FAQ does not exist", async () => { + const faqId = "faq-123"; + + faqRepository.findOne.mockResolvedValue(null); + + await expect(faqService.deleteFaq(faqId)).rejects.toThrow(BadRequest); + }); + + it("should throw HttpError if deletion fails", async () => { + const faqId = "faq-123"; + const existingFaq = { + id: faqId, + question: "FAQ question", + answer: "Answer", + category: "General", + } as FAQ; + + faqRepository.findOne.mockResolvedValue(existingFaq); + faqRepository.delete.mockResolvedValue({ affected: 0, raw: [] }); + }); + }); +}); diff --git a/src/test/getUserByAdmin.spec.ts b/src/test/getUserByAdmin.spec.ts index 0c36fad9..40cd19fb 100644 --- a/src/test/getUserByAdmin.spec.ts +++ b/src/test/getUserByAdmin.spec.ts @@ -1,92 +1,92 @@ -// @ts-nocheck - -import { User } from "../models/user"; -import AppDataSource from "../data-source"; -import { AdminUserService } from "../services/admin.services"; -import { HttpError } from "../middleware"; - -jest.mock("../data-source"); - -describe("AdminUserService - getSingleUser", () => { - let adminUserService: AdminUserService; - - beforeEach(() => { - adminUserService = new AdminUserService(); - }); - - it("should return a user when found", async () => { - const mockUser = { - id: "1", - profile: { - first_name: "Precious", - last_name: "Ifeaka", - phone_number: "1234567890", - avatarUrl: "http://example.com/avatar.jpg", - }, - email: "ifeakaa@example.com", - role: "user", - } as User; - - const userRepository = { - findOne: jest.fn().mockResolvedValue(mockUser), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue(userRepository); - - const result = await adminUserService.getSingleUser("1"); - - expect(result).toEqual(mockUser); - expect(userRepository.findOne).toHaveBeenCalledWith({ - where: { id: "1" }, - }); - }); - - it("should throw a 404 error if user is not found", async () => { - const userRepository = { - findOne: jest.fn().mockResolvedValue(null), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue(userRepository); - - await expect(adminUserService.getSingleUser("1")).rejects.toThrow( - HttpError, - ); - await expect(adminUserService.getSingleUser("1")).rejects.toThrow( - "User not found", - ); - }); - - it("should handle internal server error", async () => { - const userRepository = { - findOne: jest.fn().mockImplementation(() => { - throw new Error("Internal server error"); - }), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue(userRepository); - - await expect(adminUserService.getSingleUser("1")).rejects.toThrow( - HttpError, - ); - await expect(adminUserService.getSingleUser("1")).rejects.toThrow( - "Internal server error", - ); - }); - - it("should handle any other error", async () => { - const userRepository = { - findOne: jest.fn().mockImplementation(() => { - throw new Error("Unexpected error"); - }), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue(userRepository); - - await expect(adminUserService.getSingleUser("1")).rejects.toThrow( - HttpError, - ); - await expect(adminUserService.getSingleUser("1")).rejects.toThrow( - "Unexpected error", - ); - }); -}); +// @ts-nocheck + +import { User } from "../models/user"; +import AppDataSource from "../data-source"; +import { AdminUserService } from "../services/admin.services"; +import { HttpError } from "../middleware"; + +jest.mock("../data-source"); + +describe("AdminUserService - getSingleUser", () => { + let adminUserService: AdminUserService; + + beforeEach(() => { + adminUserService = new AdminUserService(); + }); + + it("should return a user when found", async () => { + const mockUser = { + id: "1", + profile: { + first_name: "Precious", + last_name: "Ifeaka", + phone_number: "1234567890", + avatarUrl: "http://example.com/avatar.jpg", + }, + email: "ifeakaa@example.com", + role: "user", + } as User; + + const userRepository = { + findOne: jest.fn().mockResolvedValue(mockUser), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue(userRepository); + + const result = await adminUserService.getSingleUser("1"); + + expect(result).toEqual(mockUser); + expect(userRepository.findOne).toHaveBeenCalledWith({ + where: { id: "1" }, + }); + }); + + it("should throw a 404 error if user is not found", async () => { + const userRepository = { + findOne: jest.fn().mockResolvedValue(null), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue(userRepository); + + await expect(adminUserService.getSingleUser("1")).rejects.toThrow( + HttpError, + ); + await expect(adminUserService.getSingleUser("1")).rejects.toThrow( + "User not found", + ); + }); + + it("should handle internal server error", async () => { + const userRepository = { + findOne: jest.fn().mockImplementation(() => { + throw new Error("Internal server error"); + }), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue(userRepository); + + await expect(adminUserService.getSingleUser("1")).rejects.toThrow( + HttpError, + ); + await expect(adminUserService.getSingleUser("1")).rejects.toThrow( + "Internal server error", + ); + }); + + it("should handle any other error", async () => { + const userRepository = { + findOne: jest.fn().mockImplementation(() => { + throw new Error("Unexpected error"); + }), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue(userRepository); + + await expect(adminUserService.getSingleUser("1")).rejects.toThrow( + HttpError, + ); + await expect(adminUserService.getSingleUser("1")).rejects.toThrow( + "Unexpected error", + ); + }); +}); diff --git a/src/test/help-center.spec.ts b/src/test/help-center.spec.ts index 95b07922..26f4988b 100644 --- a/src/test/help-center.spec.ts +++ b/src/test/help-center.spec.ts @@ -1,143 +1,143 @@ -//@ts-nocheck - -import { HelpService } from "../services"; -import { HelpCenterTopic } from "../models"; -import { Repository } from "typeorm"; -import AppDataSource from "../data-source"; -import { HttpError } from "../middleware"; - -jest.mock("../data-source", () => { - return { - getRepository: jest.fn(), - }; -}); - -describe("HelpService", () => { - let helpService: HelpService; - let helpRepository: Repository; - - beforeEach(() => { - helpRepository = { - findOne: jest.fn(), - create: jest.fn(), - save: jest.fn(), - find: jest.fn(), - update: jest.fn(), - } as any; - (AppDataSource.getRepository as jest.Mock).mockReturnValue(helpRepository); - helpService = new HelpService(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("create", () => { - it("should create a new article", async () => { - const title = "New Article"; - const content = "Content of the new article"; - const author = "Author"; - const article = { id: "1", title, content, author }; - - (helpRepository.findOne as jest.Mock).mockResolvedValue(null); - (helpRepository.create as jest.Mock).mockReturnValue(article); - (helpRepository.save as jest.Mock).mockResolvedValue(article); - - const result = await helpService.create(title, content, author); - - expect(helpRepository.findOne).toHaveBeenCalledWith({ - where: { title }, - }); - expect(helpRepository.create).toHaveBeenCalledWith({ - title, - content, - author, - }); - expect(helpRepository.save).toHaveBeenCalledWith(article); - expect(result).toEqual(article); - }); - - it("should throw an error if the title already exists", async () => { - const title = "Existing Article"; - const content = "Content"; - const author = "Author"; - const existingArticle = { id: "1", title, content, author }; - - (helpRepository.findOne as jest.Mock).mockResolvedValue(existingArticle); - - await expect(helpService.create(title, content, author)).rejects.toThrow( - HttpError, - ); - }); - }); - - describe("getAll", () => { - it("should return all articles", async () => { - const articles = [ - { - id: "1", - title: "Article 1", - content: "Content 1", - author: "Author 1", - }, - { - id: "2", - title: "Article 2", - content: "Content 2", - author: "Author 2", - }, - ]; - - (helpRepository.find as jest.Mock).mockResolvedValue(articles); - - const result = await helpService.getAll(); - - expect(helpRepository.find).toHaveBeenCalled(); - expect(result).toEqual(articles); - }); - }); - - describe("update", () => { - it("should throw an error if the article does not exist", async () => { - const id = "non-existing-id"; - const title = "Title"; - const content = "Content"; - const author = "Author"; - - (helpRepository.findOne as jest.Mock).mockResolvedValue(null); - - await expect( - helpService.update(id, title, content, author), - ).rejects.toThrow(HttpError); - }); - }); - - describe("getTopicById", () => { - it("should return the article if it exists", async () => { - const id = "1"; - const existingArticle = { - id, - title: "Title", - content: "Content", - author: "Author", - }; - - (helpRepository.findOne as jest.Mock).mockResolvedValue(existingArticle); - - const result = await helpService.getTopicById(id); - - expect(helpRepository.findOne).toHaveBeenCalledWith({ where: { id } }); - expect(result).toEqual(existingArticle); - }); - - it("should throw a HttpError if the article does not exist", async () => { - const id = "non-existing-id"; - - (helpRepository.findOne as jest.Mock).mockResolvedValue(null); - - await expect(helpService.getTopicById(id)).rejects.toThrow(HttpError); - await expect(helpService.getTopicById(id)).rejects.toThrow("Not Found"); - expect(helpRepository.findOne).toHaveBeenCalledWith({ where: { id } }); - }); - }); -}); +//@ts-nocheck + +import { HelpService } from "../services"; +import { HelpCenterTopic } from "../models"; +import { Repository } from "typeorm"; +import AppDataSource from "../data-source"; +import { HttpError } from "../middleware"; + +jest.mock("../data-source", () => { + return { + getRepository: jest.fn(), + }; +}); + +describe("HelpService", () => { + let helpService: HelpService; + let helpRepository: Repository; + + beforeEach(() => { + helpRepository = { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + find: jest.fn(), + update: jest.fn(), + } as any; + (AppDataSource.getRepository as jest.Mock).mockReturnValue(helpRepository); + helpService = new HelpService(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("create", () => { + it("should create a new article", async () => { + const title = "New Article"; + const content = "Content of the new article"; + const author = "Author"; + const article = { id: "1", title, content, author }; + + (helpRepository.findOne as jest.Mock).mockResolvedValue(null); + (helpRepository.create as jest.Mock).mockReturnValue(article); + (helpRepository.save as jest.Mock).mockResolvedValue(article); + + const result = await helpService.create(title, content, author); + + expect(helpRepository.findOne).toHaveBeenCalledWith({ + where: { title }, + }); + expect(helpRepository.create).toHaveBeenCalledWith({ + title, + content, + author, + }); + expect(helpRepository.save).toHaveBeenCalledWith(article); + expect(result).toEqual(article); + }); + + it("should throw an error if the title already exists", async () => { + const title = "Existing Article"; + const content = "Content"; + const author = "Author"; + const existingArticle = { id: "1", title, content, author }; + + (helpRepository.findOne as jest.Mock).mockResolvedValue(existingArticle); + + await expect(helpService.create(title, content, author)).rejects.toThrow( + HttpError, + ); + }); + }); + + describe("getAll", () => { + it("should return all articles", async () => { + const articles = [ + { + id: "1", + title: "Article 1", + content: "Content 1", + author: "Author 1", + }, + { + id: "2", + title: "Article 2", + content: "Content 2", + author: "Author 2", + }, + ]; + + (helpRepository.find as jest.Mock).mockResolvedValue(articles); + + const result = await helpService.getAll(); + + expect(helpRepository.find).toHaveBeenCalled(); + expect(result).toEqual(articles); + }); + }); + + describe("update", () => { + it("should throw an error if the article does not exist", async () => { + const id = "non-existing-id"; + const title = "Title"; + const content = "Content"; + const author = "Author"; + + (helpRepository.findOne as jest.Mock).mockResolvedValue(null); + + await expect( + helpService.update(id, title, content, author), + ).rejects.toThrow(HttpError); + }); + }); + + describe("getTopicById", () => { + it("should return the article if it exists", async () => { + const id = "1"; + const existingArticle = { + id, + title: "Title", + content: "Content", + author: "Author", + }; + + (helpRepository.findOne as jest.Mock).mockResolvedValue(existingArticle); + + const result = await helpService.getTopicById(id); + + expect(helpRepository.findOne).toHaveBeenCalledWith({ where: { id } }); + expect(result).toEqual(existingArticle); + }); + + it("should throw a HttpError if the article does not exist", async () => { + const id = "non-existing-id"; + + (helpRepository.findOne as jest.Mock).mockResolvedValue(null); + + await expect(helpService.getTopicById(id)).rejects.toThrow(HttpError); + await expect(helpService.getTopicById(id)).rejects.toThrow("Not Found"); + expect(helpRepository.findOne).toHaveBeenCalledWith({ where: { id } }); + }); + }); +}); diff --git a/src/test/invitation.spec.ts b/src/test/invitation.spec.ts index 76380ced..2d3214bd 100644 --- a/src/test/invitation.spec.ts +++ b/src/test/invitation.spec.ts @@ -1,238 +1,238 @@ -import { OrgService } from "../services"; -import { Repository } from "typeorm"; -import { Invitation, Organization, User, UserOrganization } from "../models"; -import AppDataSource from "../data-source"; -import { ResourceNotFound, Conflict } from "../middleware"; -import { addEmailToQueue } from "../utils/queue"; -import config from "../config"; - -jest.mock("../data-source"); -jest.mock("../utils/queue"); -jest.mock("../views/email/renderTemplate"); -jest.mock("uuid", () => ({ v4: jest.fn(() => "some-uuid-token") })); - -const frontendBaseUrl = config.BASE_URL; - -describe("InviteService", () => { - let inviteService: OrgService; - let inviteRepository: jest.Mocked>; - let organizationRepository: jest.Mocked>; - let userOrganizationRepository: jest.Mocked>; - let userRepository: jest.Mocked>; - - beforeEach(() => { - inviteRepository = { - findOne: jest.fn(), - findAndCount: jest.fn(), - create: jest.fn(), - save: jest.fn(), - } as any; - organizationRepository = { - findOne: jest.fn(), - } as any; - userOrganizationRepository = { - findOne: jest.fn(), - create: jest.fn(), - save: jest.fn(), - } as any; - userRepository = { - findOne: jest.fn(), - } as any; - - AppDataSource.getRepository = jest.fn().mockImplementation((model) => { - switch (model) { - case Invitation: - return inviteRepository; - case Organization: - return organizationRepository; - case UserOrganization: - return userOrganizationRepository; - case User: - return userRepository; - } - }); - - inviteService = new OrgService(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("generateGenericInviteLink", () => { - it("should generate a generic invite link", async () => { - const organizationId = "org-123"; - const organization = { id: organizationId } as Organization; - - organizationRepository.findOne.mockResolvedValue(organization); - inviteRepository.create.mockReturnValue({ - token: "some-uuid-token", - isGeneric: true, - organization, - } as Invitation); - - const result = - await inviteService.generateGenericInviteLink(organizationId); - - expect(organizationRepository.findOne).toHaveBeenCalledWith({ - where: { id: organizationId }, - }); - expect(inviteRepository.create).toHaveBeenCalledWith({ - token: "some-uuid-token", - isGeneric: true, - organization: { id: organizationId }, - }); - expect(inviteRepository.save).toHaveBeenCalledWith({ - token: "some-uuid-token", - isGeneric: true, - organization, - }); - expect(result).toBe(`${frontendBaseUrl}/invite?token=some-uuid-token`); - }); - - it("should throw ResourceNotFound if organization does not exist", async () => { - const organizationId = "org-123"; - - organizationRepository.findOne.mockResolvedValue(null); - - await expect( - inviteService.generateGenericInviteLink(organizationId), - ).rejects.toThrow(ResourceNotFound); - }); - }); - - describe("generateAndSendInviteLinks", () => { - it("should generate and send invite links", async () => { - const organizationId = "org-123"; - const emails = ["test1@example.com", "test2@example.com"]; - const organization = { - id: organizationId, - name: "Test Organization", - } as Organization; - - organizationRepository.findOne.mockResolvedValue(organization); - inviteRepository.create.mockImplementation( - (invite) => - ({ - ...invite, - token: "some-uuid-token", - email: invite.email, - isGeneric: false, - organization: invite.organization, - }) as Invitation, - ); - inviteRepository.save.mockResolvedValue([ - { - token: "some-uuid-token", - email: emails[0], - isGeneric: false, - organization, - id: "invite-id", - isAccepted: false, - }, - { - token: "some-uuid-token-2", - email: emails[1], - isGeneric: false, - organization, - id: "invite-id-2", - isAccepted: false, - }, - ] as unknown as Invitation); - await inviteService.generateAndSendInviteLinks(emails, organizationId); - - expect(organizationRepository.findOne).toHaveBeenCalledWith({ - where: { id: organizationId }, - }); - expect(inviteRepository.save).toHaveBeenCalledWith(expect.any(Array)); - expect(addEmailToQueue).toHaveBeenCalledTimes(emails.length); - }); - - it("should throw ResourceNotFound if organization does not exist", async () => { - const organizationId = "org-123"; - const emails = ["test1@example.com", "test2@example.com"]; - - organizationRepository.findOne.mockResolvedValue(null); - - await expect( - inviteService.generateAndSendInviteLinks(emails, organizationId), - ).rejects.toThrow(ResourceNotFound); - }); - }); - - describe("getAllInvite", () => { - it("should return paginated invites", async () => { - const mockInvites: Invitation[] = [ - { - id: "1", - token: "token1", - email: "email1@example.com", - isGeneric: false, - isAccepted: false, - organization: new Organization(), - created_at: new Date(), - updated_at: new Date(), - }, - { - id: "2", - token: "token2", - email: "email2@example.com", - isGeneric: false, - isAccepted: false, - organization: new Organization(), - created_at: new Date(), - updated_at: new Date(), - }, - ]; - const mockTotal = 2; - - inviteRepository.findAndCount.mockResolvedValue([mockInvites, mockTotal]); - - const page = 1; - const pageSize = 2; - - const result = await inviteService.getAllInvite(page, pageSize); - - expect(inviteRepository.findAndCount).toHaveBeenCalledWith({ - skip: (page - 1) * pageSize, - take: pageSize, - }); - expect(result).toEqual({ - status_code: 200, - message: "Successfully fetched invites", - data: mockInvites.map((invite) => ({ - id: invite.id, - token: invite.token, - isAccepted: invite.isAccepted, - isGeneric: invite.isGeneric, - organization: invite.organization, - email: invite.email, - })), - total: mockTotal, - }); - }); - - it("should return no invites when none exist", async () => { - const mockInvites: Invitation[] = []; - const mockTotal = 0; - - inviteRepository.findAndCount.mockResolvedValue([mockInvites, mockTotal]); - - const page = 1; - const pageSize = 2; - - const result = await inviteService.getAllInvite(page, pageSize); - - expect(inviteRepository.findAndCount).toHaveBeenCalledWith({ - skip: (page - 1) * pageSize, - take: pageSize, - }); - expect(result).toEqual({ - status_code: 200, - message: "No invites yet", - data: mockInvites, - total: mockTotal, - }); - }); - }); -}); +import { OrgService } from "../services"; +import { Repository } from "typeorm"; +import { Invitation, Organization, User, UserOrganization } from "../models"; +import AppDataSource from "../data-source"; +import { ResourceNotFound, Conflict } from "../middleware"; +import { addEmailToQueue } from "../utils/queue"; +import config from "../config"; + +jest.mock("../data-source"); +jest.mock("../utils/queue"); +jest.mock("../views/email/renderTemplate"); +jest.mock("uuid", () => ({ v4: jest.fn(() => "some-uuid-token") })); + +const frontendBaseUrl = config.BASE_URL; + +describe("InviteService", () => { + let inviteService: OrgService; + let inviteRepository: jest.Mocked>; + let organizationRepository: jest.Mocked>; + let userOrganizationRepository: jest.Mocked>; + let userRepository: jest.Mocked>; + + beforeEach(() => { + inviteRepository = { + findOne: jest.fn(), + findAndCount: jest.fn(), + create: jest.fn(), + save: jest.fn(), + } as any; + organizationRepository = { + findOne: jest.fn(), + } as any; + userOrganizationRepository = { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + } as any; + userRepository = { + findOne: jest.fn(), + } as any; + + AppDataSource.getRepository = jest.fn().mockImplementation((model) => { + switch (model) { + case Invitation: + return inviteRepository; + case Organization: + return organizationRepository; + case UserOrganization: + return userOrganizationRepository; + case User: + return userRepository; + } + }); + + inviteService = new OrgService(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("generateGenericInviteLink", () => { + it("should generate a generic invite link", async () => { + const organizationId = "org-123"; + const organization = { id: organizationId } as Organization; + + organizationRepository.findOne.mockResolvedValue(organization); + inviteRepository.create.mockReturnValue({ + token: "some-uuid-token", + isGeneric: true, + organization, + } as Invitation); + + const result = + await inviteService.generateGenericInviteLink(organizationId); + + expect(organizationRepository.findOne).toHaveBeenCalledWith({ + where: { id: organizationId }, + }); + expect(inviteRepository.create).toHaveBeenCalledWith({ + token: "some-uuid-token", + isGeneric: true, + organization: { id: organizationId }, + }); + expect(inviteRepository.save).toHaveBeenCalledWith({ + token: "some-uuid-token", + isGeneric: true, + organization, + }); + expect(result).toBe(`${frontendBaseUrl}/invite?token=some-uuid-token`); + }); + + it("should throw ResourceNotFound if organization does not exist", async () => { + const organizationId = "org-123"; + + organizationRepository.findOne.mockResolvedValue(null); + + await expect( + inviteService.generateGenericInviteLink(organizationId), + ).rejects.toThrow(ResourceNotFound); + }); + }); + + describe("generateAndSendInviteLinks", () => { + it("should generate and send invite links", async () => { + const organizationId = "org-123"; + const emails = ["test1@example.com", "test2@example.com"]; + const organization = { + id: organizationId, + name: "Test Organization", + } as Organization; + + organizationRepository.findOne.mockResolvedValue(organization); + inviteRepository.create.mockImplementation( + (invite) => + ({ + ...invite, + token: "some-uuid-token", + email: invite.email, + isGeneric: false, + organization: invite.organization, + }) as Invitation, + ); + inviteRepository.save.mockResolvedValue([ + { + token: "some-uuid-token", + email: emails[0], + isGeneric: false, + organization, + id: "invite-id", + isAccepted: false, + }, + { + token: "some-uuid-token-2", + email: emails[1], + isGeneric: false, + organization, + id: "invite-id-2", + isAccepted: false, + }, + ] as unknown as Invitation); + await inviteService.generateAndSendInviteLinks(emails, organizationId); + + expect(organizationRepository.findOne).toHaveBeenCalledWith({ + where: { id: organizationId }, + }); + expect(inviteRepository.save).toHaveBeenCalledWith(expect.any(Array)); + expect(addEmailToQueue).toHaveBeenCalledTimes(emails.length); + }); + + it("should throw ResourceNotFound if organization does not exist", async () => { + const organizationId = "org-123"; + const emails = ["test1@example.com", "test2@example.com"]; + + organizationRepository.findOne.mockResolvedValue(null); + + await expect( + inviteService.generateAndSendInviteLinks(emails, organizationId), + ).rejects.toThrow(ResourceNotFound); + }); + }); + + describe("getAllInvite", () => { + it("should return paginated invites", async () => { + const mockInvites: Invitation[] = [ + { + id: "1", + token: "token1", + email: "email1@example.com", + isGeneric: false, + isAccepted: false, + organization: new Organization(), + created_at: new Date(), + updated_at: new Date(), + }, + { + id: "2", + token: "token2", + email: "email2@example.com", + isGeneric: false, + isAccepted: false, + organization: new Organization(), + created_at: new Date(), + updated_at: new Date(), + }, + ]; + const mockTotal = 2; + + inviteRepository.findAndCount.mockResolvedValue([mockInvites, mockTotal]); + + const page = 1; + const pageSize = 2; + + const result = await inviteService.getAllInvite(page, pageSize); + + expect(inviteRepository.findAndCount).toHaveBeenCalledWith({ + skip: (page - 1) * pageSize, + take: pageSize, + }); + expect(result).toEqual({ + status_code: 200, + message: "Successfully fetched invites", + data: mockInvites.map((invite) => ({ + id: invite.id, + token: invite.token, + isAccepted: invite.isAccepted, + isGeneric: invite.isGeneric, + organization: invite.organization, + email: invite.email, + })), + total: mockTotal, + }); + }); + + it("should return no invites when none exist", async () => { + const mockInvites: Invitation[] = []; + const mockTotal = 0; + + inviteRepository.findAndCount.mockResolvedValue([mockInvites, mockTotal]); + + const page = 1; + const pageSize = 2; + + const result = await inviteService.getAllInvite(page, pageSize); + + expect(inviteRepository.findAndCount).toHaveBeenCalledWith({ + skip: (page - 1) * pageSize, + take: pageSize, + }); + expect(result).toEqual({ + status_code: 200, + message: "No invites yet", + data: mockInvites, + total: mockTotal, + }); + }); + }); +}); diff --git a/src/test/newsLetterSubscription.spec.ts b/src/test/newsLetterSubscription.spec.ts index 6f5f3e09..74ef3cd3 100644 --- a/src/test/newsLetterSubscription.spec.ts +++ b/src/test/newsLetterSubscription.spec.ts @@ -1,239 +1,239 @@ -import { Repository } from "typeorm"; -import AppDataSource from "../data-source"; -import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; -import { NewsLetterSubscriptionService } from "../services/newsLetterSubscription.service"; -import { BadRequest, ResourceNotFound } from "../middleware"; - -jest.mock("../data-source", () => ({ - __esModule: true, - default: { - getRepository: jest.fn(), - initialize: jest.fn(), - isInitialized: false, - }, -})); - -jest.mock("../utils", () => ({ - ...jest.requireActual("../utils"), - verifyToken: jest.fn(), -})); - -jest.mock("../models"); -jest.mock("../utils"); - -describe("NewsLetterSubscriptionService", () => { - let newsLetterSubscriptionService: NewsLetterSubscriptionService; - let newsLetterRepositoryMock: jest.Mocked>; - - beforeEach(() => { - newsLetterRepositoryMock = { - findOne: jest.fn(), - findAndCount: jest.fn(), - save: jest.fn(), - } as any; - (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { - if (entity === NewsLetterSubscriber) return newsLetterRepositoryMock; - }); - - newsLetterSubscriptionService = new NewsLetterSubscriptionService(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("SubscribeToNewsLetter", () => { - it("should subscribe a new user", async () => { - const newSubscriber = new NewsLetterSubscriber(); - newSubscriber.email = "test1@example.com"; - - (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(null); - (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( - (user) => { - user.id = "456"; - return Promise.resolve(user); - }, - ); - - const result = - await newsLetterSubscriptionService.subscribeUser("test1@example.com"); - - expect(result.isNewlySubscribe).toBe(true); - expect(result.subscriber).toEqual({ - id: "456", - email: "test1@example.com", - isSubscribe: true, - }); - expect(newsLetterRepositoryMock.save).toHaveBeenCalledWith( - expect.objectContaining({ - email: "test1@example.com", - isSubscribe: true, - }), - ); - }); - - it("should handle already subscribed user", async () => { - const user = new NewsLetterSubscriber(); - user.id = "123"; - user.email = "test@example.com"; - user.isSubscribe = true; - (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(user); - (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( - (user) => { - user.id = "456"; - return Promise.resolve(user); - }, - ); - - const result = - await newsLetterSubscriptionService.subscribeUser("test@example.com"); - - expect(result.isNewlySubscribe).toBe(false); - expect(result.subscriber).toEqual({ - id: "123", - email: "test@example.com", - isSubscribe: true, - }); - expect(newsLetterRepositoryMock.save).not.toHaveBeenCalled(); - }); - - it("should throw a Conflict error if already subscribed but inactive", async () => { - const inactiveSubscriber = new NewsLetterSubscriber(); - inactiveSubscriber.email = "test@example.com"; - inactiveSubscriber.isSubscribe = false; - - (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue( - inactiveSubscriber, - ); - - await expect( - newsLetterSubscriptionService.subscribeUser("test@example.com"), - ).rejects.toThrow(BadRequest); - }); - - it("should throw an error if something goes wrong", async () => { - (newsLetterRepositoryMock.findOne as jest.Mock).mockRejectedValue( - new Error("An error occurred while processing your request"), - ); - - await expect( - newsLetterSubscriptionService.subscribeUser("test@example.com"), - ).rejects.toThrow("An error occurred while processing your request"); - }); - }); - - describe("fetchAllNewsletter", () => { - it("should fetch all newsletters with pagination", async () => { - const page = 2; - const limit = 20; - const mockSubscribers: any = [ - { id: "1", email: "user1@example.com" }, - { id: "2", email: "user2@example.com" }, - { id: "3", email: "user3@example.com" }, - ] as unknown as NewsLetterSubscriber[]; - const mockTotal = 50; - - newsLetterRepositoryMock.findAndCount.mockResolvedValue([ - mockSubscribers, - mockTotal, - ]); - - const result = await newsLetterSubscriptionService.fetchAllNewsletter({ - page, - limit, - }); - - expect(result).toEqual({ - data: mockSubscribers, - meta: { - total: mockTotal, - page, - limit, - totalPages: Math.ceil(mockTotal / limit), - }, - }); - expect(newsLetterRepositoryMock.findAndCount).toHaveBeenCalledWith({ - skip: (page - 1) * limit, - take: limit, - }); - }); - - it("should handle default pagination values", async () => { - const mockSubscribers: any = [ - { id: "1", email: "user1@example.com" }, - { id: "2", email: "user2@example.com" }, - ]; - const mockTotal = 20; - - newsLetterRepositoryMock.findAndCount.mockResolvedValue([ - mockSubscribers, - mockTotal, - ]); - - const result = await newsLetterSubscriptionService.fetchAllNewsletter({}); - - expect(result).toEqual({ - data: mockSubscribers, - meta: { - total: mockTotal, - page: 1, - limit: 10, - totalPages: 2, - }, - }); - expect(newsLetterRepositoryMock.findAndCount).toHaveBeenCalledWith({ - skip: 0, - take: 10, - }); - }); - }); - - describe("UnsubscribeFromNewsLetter", () => { - it("should successfully unsubscribe a logged-in user from the newsletter", async () => { - const user = new NewsLetterSubscriber(); - user.email = "test1@example.com"; - user.id = "5678"; - user.isSubscribe = true; - - (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(user); - - (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( - (user) => { - user.isSubscribe = false; - return Promise.resolve(user); - }, - ); - - const result = - await newsLetterSubscriptionService.unSubcribeUser("test1@example.com"); - - expect(result).toEqual({ - id: "5678", - email: "test1@example.com", - isSubscribe: false, - }); - - expect(newsLetterRepositoryMock.save).toHaveBeenCalledWith( - expect.objectContaining({ - id: "5678", - email: "test1@example.com", - isSubscribe: false, - }), - ); - }); - - it("should throw an error if user is not subscribed", async () => { - const inactiveSubscriber = new NewsLetterSubscriber(); - inactiveSubscriber.email = "test@example.com"; - inactiveSubscriber.isSubscribe = false; - - (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue( - inactiveSubscriber, - ); - - await expect( - newsLetterSubscriptionService.subscribeUser("test@example.com"), - ).rejects.toThrow(BadRequest); - }); - }); -}); +import { Repository } from "typeorm"; +import AppDataSource from "../data-source"; +import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; +import { NewsLetterSubscriptionService } from "../services/newsLetterSubscription.service"; +import { BadRequest, ResourceNotFound } from "../middleware"; + +jest.mock("../data-source", () => ({ + __esModule: true, + default: { + getRepository: jest.fn(), + initialize: jest.fn(), + isInitialized: false, + }, +})); + +jest.mock("../utils", () => ({ + ...jest.requireActual("../utils"), + verifyToken: jest.fn(), +})); + +jest.mock("../models"); +jest.mock("../utils"); + +describe("NewsLetterSubscriptionService", () => { + let newsLetterSubscriptionService: NewsLetterSubscriptionService; + let newsLetterRepositoryMock: jest.Mocked>; + + beforeEach(() => { + newsLetterRepositoryMock = { + findOne: jest.fn(), + findAndCount: jest.fn(), + save: jest.fn(), + } as any; + (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { + if (entity === NewsLetterSubscriber) return newsLetterRepositoryMock; + }); + + newsLetterSubscriptionService = new NewsLetterSubscriptionService(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("SubscribeToNewsLetter", () => { + it("should subscribe a new user", async () => { + const newSubscriber = new NewsLetterSubscriber(); + newSubscriber.email = "test1@example.com"; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(null); + (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( + (user) => { + user.id = "456"; + return Promise.resolve(user); + }, + ); + + const result = + await newsLetterSubscriptionService.subscribeUser("test1@example.com"); + + expect(result.isNewlySubscribe).toBe(true); + expect(result.subscriber).toEqual({ + id: "456", + email: "test1@example.com", + isSubscribe: true, + }); + expect(newsLetterRepositoryMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + email: "test1@example.com", + isSubscribe: true, + }), + ); + }); + + it("should handle already subscribed user", async () => { + const user = new NewsLetterSubscriber(); + user.id = "123"; + user.email = "test@example.com"; + user.isSubscribe = true; + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(user); + (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( + (user) => { + user.id = "456"; + return Promise.resolve(user); + }, + ); + + const result = + await newsLetterSubscriptionService.subscribeUser("test@example.com"); + + expect(result.isNewlySubscribe).toBe(false); + expect(result.subscriber).toEqual({ + id: "123", + email: "test@example.com", + isSubscribe: true, + }); + expect(newsLetterRepositoryMock.save).not.toHaveBeenCalled(); + }); + + it("should throw a Conflict error if already subscribed but inactive", async () => { + const inactiveSubscriber = new NewsLetterSubscriber(); + inactiveSubscriber.email = "test@example.com"; + inactiveSubscriber.isSubscribe = false; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue( + inactiveSubscriber, + ); + + await expect( + newsLetterSubscriptionService.subscribeUser("test@example.com"), + ).rejects.toThrow(BadRequest); + }); + + it("should throw an error if something goes wrong", async () => { + (newsLetterRepositoryMock.findOne as jest.Mock).mockRejectedValue( + new Error("An error occurred while processing your request"), + ); + + await expect( + newsLetterSubscriptionService.subscribeUser("test@example.com"), + ).rejects.toThrow("An error occurred while processing your request"); + }); + }); + + describe("fetchAllNewsletter", () => { + it("should fetch all newsletters with pagination", async () => { + const page = 2; + const limit = 20; + const mockSubscribers: any = [ + { id: "1", email: "user1@example.com" }, + { id: "2", email: "user2@example.com" }, + { id: "3", email: "user3@example.com" }, + ] as unknown as NewsLetterSubscriber[]; + const mockTotal = 50; + + newsLetterRepositoryMock.findAndCount.mockResolvedValue([ + mockSubscribers, + mockTotal, + ]); + + const result = await newsLetterSubscriptionService.fetchAllNewsletter({ + page, + limit, + }); + + expect(result).toEqual({ + data: mockSubscribers, + meta: { + total: mockTotal, + page, + limit, + totalPages: Math.ceil(mockTotal / limit), + }, + }); + expect(newsLetterRepositoryMock.findAndCount).toHaveBeenCalledWith({ + skip: (page - 1) * limit, + take: limit, + }); + }); + + it("should handle default pagination values", async () => { + const mockSubscribers: any = [ + { id: "1", email: "user1@example.com" }, + { id: "2", email: "user2@example.com" }, + ]; + const mockTotal = 20; + + newsLetterRepositoryMock.findAndCount.mockResolvedValue([ + mockSubscribers, + mockTotal, + ]); + + const result = await newsLetterSubscriptionService.fetchAllNewsletter({}); + + expect(result).toEqual({ + data: mockSubscribers, + meta: { + total: mockTotal, + page: 1, + limit: 10, + totalPages: 2, + }, + }); + expect(newsLetterRepositoryMock.findAndCount).toHaveBeenCalledWith({ + skip: 0, + take: 10, + }); + }); + }); + + describe("UnsubscribeFromNewsLetter", () => { + it("should successfully unsubscribe a logged-in user from the newsletter", async () => { + const user = new NewsLetterSubscriber(); + user.email = "test1@example.com"; + user.id = "5678"; + user.isSubscribe = true; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(user); + + (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( + (user) => { + user.isSubscribe = false; + return Promise.resolve(user); + }, + ); + + const result = + await newsLetterSubscriptionService.unSubcribeUser("test1@example.com"); + + expect(result).toEqual({ + id: "5678", + email: "test1@example.com", + isSubscribe: false, + }); + + expect(newsLetterRepositoryMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: "5678", + email: "test1@example.com", + isSubscribe: false, + }), + ); + }); + + it("should throw an error if user is not subscribed", async () => { + const inactiveSubscriber = new NewsLetterSubscriber(); + inactiveSubscriber.email = "test@example.com"; + inactiveSubscriber.isSubscribe = false; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue( + inactiveSubscriber, + ); + + await expect( + newsLetterSubscriptionService.subscribeUser("test@example.com"), + ).rejects.toThrow(BadRequest); + }); + }); +}); diff --git a/src/test/notification.spec.ts b/src/test/notification.spec.ts index 475a060d..d7aa8ab8 100644 --- a/src/test/notification.spec.ts +++ b/src/test/notification.spec.ts @@ -1,47 +1,47 @@ -import { Repository } from "typeorm"; -import { NotificationsService } from "../services"; -import { Notification } from "../models"; -import { mock, MockProxy } from "jest-mock-extended"; - -describe("NotificationsService", () => { - let notificationsService: NotificationsService; - let notificationRepository: MockProxy>; - - beforeEach(() => { - notificationRepository = mock>(); - notificationsService = new NotificationsService(); - (notificationsService as any).notificationRepository = - notificationRepository; - }); - - describe("getNotificationsForUser", () => { - it("should return the correct notification counts and list of notifications", async () => { - const userId = "some-user-id"; - const mockNotifications = [ - { id: "1", isRead: false, createdAt: new Date(), user: { id: userId } }, - { id: "2", isRead: true, createdAt: new Date(), user: { id: userId } }, - { id: "3", isRead: false, createdAt: new Date(), user: { id: userId } }, - ] as any; - - notificationRepository.find.mockResolvedValue(mockNotifications); - - const result = await notificationsService.getNotificationsForUser(userId); - - expect(result.totalNotificationCount).toBe(3); - expect(result.totalUnreadNotificationCount).toBe(2); - expect(result.notifications).toEqual(mockNotifications); - }); - - it("should return empty counts and list if no notifications are found", async () => { - const userId = "some-user-id"; - - notificationRepository.find.mockResolvedValue([]); - - const result = await notificationsService.getNotificationsForUser(userId); - - expect(result.totalNotificationCount).toBe(0); - expect(result.totalUnreadNotificationCount).toBe(0); - expect(result.notifications).toEqual([]); - }); - }); -}); +import { Repository } from "typeorm"; +import { NotificationsService } from "../services"; +import { Notification } from "../models"; +import { mock, MockProxy } from "jest-mock-extended"; + +describe("NotificationsService", () => { + let notificationsService: NotificationsService; + let notificationRepository: MockProxy>; + + beforeEach(() => { + notificationRepository = mock>(); + notificationsService = new NotificationsService(); + (notificationsService as any).notificationRepository = + notificationRepository; + }); + + describe("getNotificationsForUser", () => { + it("should return the correct notification counts and list of notifications", async () => { + const userId = "some-user-id"; + const mockNotifications = [ + { id: "1", isRead: false, createdAt: new Date(), user: { id: userId } }, + { id: "2", isRead: true, createdAt: new Date(), user: { id: userId } }, + { id: "3", isRead: false, createdAt: new Date(), user: { id: userId } }, + ] as any; + + notificationRepository.find.mockResolvedValue(mockNotifications); + + const result = await notificationsService.getNotificationsForUser(userId); + + expect(result.totalNotificationCount).toBe(3); + expect(result.totalUnreadNotificationCount).toBe(2); + expect(result.notifications).toEqual(mockNotifications); + }); + + it("should return empty counts and list if no notifications are found", async () => { + const userId = "some-user-id"; + + notificationRepository.find.mockResolvedValue([]); + + const result = await notificationsService.getNotificationsForUser(userId); + + expect(result.totalNotificationCount).toBe(0); + expect(result.totalUnreadNotificationCount).toBe(0); + expect(result.notifications).toEqual([]); + }); + }); +}); diff --git a/src/test/org.spec.ts b/src/test/org.spec.ts index 11dc48dd..49e18ff0 100644 --- a/src/test/org.spec.ts +++ b/src/test/org.spec.ts @@ -1,110 +1,110 @@ -// src/__tests__/getOrganizationsByUserId.test.ts -import request from "supertest"; -import express from "express"; -import { OrgController } from "../controllers/OrgController"; -import { Organization } from "../models/organization"; -import AppDataSource from "../data-source"; -import jwt from "jsonwebtoken"; -import config from "../config"; -import dotenv from "dotenv"; -dotenv.config(); - -const tokenSecret = config.TOKEN_SECRET; -// Mock data source -jest.mock("../data-source", () => { - const actualDataSource = jest.requireActual("../data-source"); - return { - ...actualDataSource, - getRepository: jest.fn().mockReturnValue({ - find: jest.fn(), - }), - initialize: jest.fn().mockResolvedValue(null), - }; -}); - -// Mock logger -jest.mock("../utils/logger", () => ({ - info: jest.fn(), - error: jest.fn(), -})); - -const app = express(); -app.use(express.json()); -const orgController = new OrgController(); -app.get( - "/api/v1/users/:id/organizations", - orgController.getOrganizations.bind(orgController), -); - -// Test Suite -describe("GET /api/v1/users/:id/organizations", () => { - let token: string; - const userId = "1a546056-6d6b-4f4a-abc0-0a911467c8c7"; - const organizations = [ - { - id: "org1", - name: "Org One", - slug: "org-one", - owner_id: userId, - created_at: new Date(), - updated_at: new Date(), - }, - { - id: "org2", - name: "Org Two", - slug: "org-two", - owner_id: userId, - created_at: new Date(), - updated_at: new Date(), - }, - ]; - - beforeAll(() => { - token = jwt.sign({ userId }, "6789094837hfvg5hn54g8743ry894w4", { - expiresIn: "1h", - }); - const organizationRepository = AppDataSource.getRepository(Organization); - (organizationRepository.find as jest.Mock).mockResolvedValue(organizations); - }); - - it("should return 200 and the organizations for the user", async () => { - const response = await request(app) - .get(`/api/v1/users/${userId}/organizations`) - .set("Authorization", `Bearer ${token}`); - - expect(response.status).toBe(400); - expect(response.body.status).toBe("unsuccessful"); - expect(response.body.message).toBe( - "Invalid user ID or authentication mismatch.", - ); - }); - - it("should return 400 if user ID does not match token", async () => { - const response = await request(app) - .get(`/api/v1/users/invalid-user-id/organizations`) - .set("Authorization", `Bearer ${token}`); - - expect(response.status).toBe(400); - expect(response.body.status).toBe("unsuccessful"); - expect(response.body.message).toBe( - "Invalid user ID or authentication mismatch.", - ); - }); - - it("should return 500 if there is a server error", async () => { - const organizationRepository = AppDataSource.getRepository(Organization); - (organizationRepository.find as jest.Mock).mockRejectedValue( - new Error("Database error"), - ); - - const response = await request(app) - .get(`/api/v1/users/${userId}/organizations`) - .set("Authorization", `Bearer ${token}`); - - expect(response.status).toBe(400); - expect(response.body.status).toBe("unsuccessful"); - expect(response.body.message).toBe( - "Invalid user ID or authentication mismatch.", - ); - }); -}); +// src/__tests__/getOrganizationsByUserId.test.ts +import request from "supertest"; +import express from "express"; +import { OrgController } from "../controllers/OrgController"; +import { Organization } from "../models/organization"; +import AppDataSource from "../data-source"; +import jwt from "jsonwebtoken"; +import config from "../config"; +import dotenv from "dotenv"; +dotenv.config(); + +const tokenSecret = config.TOKEN_SECRET; +// Mock data source +jest.mock("../data-source", () => { + const actualDataSource = jest.requireActual("../data-source"); + return { + ...actualDataSource, + getRepository: jest.fn().mockReturnValue({ + find: jest.fn(), + }), + initialize: jest.fn().mockResolvedValue(null), + }; +}); + +// Mock logger +jest.mock("../utils/logger", () => ({ + info: jest.fn(), + error: jest.fn(), +})); + +const app = express(); +app.use(express.json()); +const orgController = new OrgController(); +app.get( + "/api/v1/users/:id/organizations", + orgController.getOrganizations.bind(orgController), +); + +// Test Suite +describe("GET /api/v1/users/:id/organizations", () => { + let token: string; + const userId = "1a546056-6d6b-4f4a-abc0-0a911467c8c7"; + const organizations = [ + { + id: "org1", + name: "Org One", + slug: "org-one", + owner_id: userId, + created_at: new Date(), + updated_at: new Date(), + }, + { + id: "org2", + name: "Org Two", + slug: "org-two", + owner_id: userId, + created_at: new Date(), + updated_at: new Date(), + }, + ]; + + beforeAll(() => { + token = jwt.sign({ userId }, "6789094837hfvg5hn54g8743ry894w4", { + expiresIn: "1h", + }); + const organizationRepository = AppDataSource.getRepository(Organization); + (organizationRepository.find as jest.Mock).mockResolvedValue(organizations); + }); + + it("should return 200 and the organizations for the user", async () => { + const response = await request(app) + .get(`/api/v1/users/${userId}/organizations`) + .set("Authorization", `Bearer ${token}`); + + expect(response.status).toBe(400); + expect(response.body.status).toBe("unsuccessful"); + expect(response.body.message).toBe( + "Invalid user ID or authentication mismatch.", + ); + }); + + it("should return 400 if user ID does not match token", async () => { + const response = await request(app) + .get(`/api/v1/users/invalid-user-id/organizations`) + .set("Authorization", `Bearer ${token}`); + + expect(response.status).toBe(400); + expect(response.body.status).toBe("unsuccessful"); + expect(response.body.message).toBe( + "Invalid user ID or authentication mismatch.", + ); + }); + + it("should return 500 if there is a server error", async () => { + const organizationRepository = AppDataSource.getRepository(Organization); + (organizationRepository.find as jest.Mock).mockRejectedValue( + new Error("Database error"), + ); + + const response = await request(app) + .get(`/api/v1/users/${userId}/organizations`) + .set("Authorization", `Bearer ${token}`); + + expect(response.status).toBe(400); + expect(response.body.status).toBe("unsuccessful"); + expect(response.body.message).toBe( + "Invalid user ID or authentication mismatch.", + ); + }); +}); diff --git a/src/test/organisation.spec.ts b/src/test/organisation.spec.ts index 2a95145f..71dbef9a 100644 --- a/src/test/organisation.spec.ts +++ b/src/test/organisation.spec.ts @@ -1,354 +1,354 @@ -// @ts-nocheck -import jwt from "jsonwebtoken"; -import { Repository } from "typeorm"; -import { OrgController } from "../controllers"; -import AppDataSource from "../data-source"; -import { authMiddleware } from "../middleware/auth"; -import { - InvalidInput, - ResourceNotFound, - ServerError, -} from "../middleware/error"; -import { validateOrgId } from "../middleware/organizationValidation"; -import { Organization, OrganizationRole, User } from "../models"; -import { OrgService } from "../services"; - -jest.mock("../data-source", () => ({ - __esModule: true, - default: { - getRepository: jest.fn(), - initialize: jest.fn(), - isInitialized: false, - }, -})); -jest.mock("jsonwebtoken"); -jest.mock("passport", () => ({ - use: jest.fn(), -})); -jest.mock("passport-google-oauth2", () => ({ - Strategy: jest.fn(), -})); - -describe("Organization Controller and Middleware", () => { - let organizationService: OrgService; - let orgController: OrgController; - let mockManager; - let organizationRepositoryMock: jest.Mocked>; - let organizationRoleRepositoryMock: jest.Mocked>; - - beforeEach(() => { - jest.clearAllMocks(); - orgController = new OrgController(); - - organizationRepositoryMock = { - findOne: jest.fn(), - } as any; - organizationRoleRepositoryMock = { - find: jest.fn(), - findOne: jest.fn(), - } as any; - (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { - if (entity === Organization) return organizationRepositoryMock; - if (entity === OrganizationRole) return organizationRoleRepositoryMock; - return {}; - }); - - organizationService = new OrgService(); - }); - - describe("getOrganization", () => { - it("check if user is authenticated", async () => { - const req = { - headers: { - authorization: "Bearer validToken", - }, - user: undefined, - } as unknown as Request; - - const res = { - status: jest.fn().mockReturnThis(), - json: jest.fn(), - } as unknown as Response; - - const next = jest.fn(); - - jwt.verify = jest.fn().mockImplementation((token, secret, callback) => { - callback(null, { userId: "user123" }); - }); - - User.findOne = jest.fn().mockResolvedValue({ - id: "donalTrump123", - email: "americaPresident@newyork.com", - }); - - await authMiddleware(req, res, next); - - expect(jwt.verify).toHaveBeenCalled(); - expect(User.findOne).toHaveBeenCalled(); - expect(req.user).toBeDefined(); - expect(req.user.id).toBe("donalTrump123"); - expect(next).toHaveBeenCalled(); - }); - - it("should get a single user org", async () => { - const orgId = "1"; - const orgRes = { - org_id: "1", - name: "Org 1", - description: "Org 1 description", - }; - - organizationRepositoryMock.findOne.mockResolvedValue(orgRes); - }); - - it("should pass valid UUID for org_id", async () => { - const req = { - params: { org_id: "123e4567-e89b-12d3-a456-426614174000" }, - } as unknown as Request; - const res = {} as Response; - const next = jest.fn(); - - await validateOrgId[0](req, res, next); - await validateOrgId[1](req, res, next); - - expect(next).toHaveBeenCalledTimes(2); - expect(next).toHaveBeenCalledWith(); - }); - - it("should throw InvalidInput for empty org_id", async () => { - const req = { - params: { org_id: "" }, - } as unknown as Request; - const res = {} as Response; - const next = jest.fn(); - - await validateOrgId[0](req, res, next); - - expect(() => validateOrgId[1](req, res, next)).toThrow(InvalidInput); - expect(() => validateOrgId[1](req, res, next)).toThrow( - "Organisation id is required", - ); - }); - - it("should throw InvalidInput for non-UUID org_id", async () => { - const req = { - params: { org_id: "donald-trump-for-president" }, - } as unknown as Request; - const res = {} as Response; - const next = jest.fn(); - - await validateOrgId[0](req, res, next); - - expect(() => validateOrgId[1](req, res, next)).toThrow(InvalidInput); - expect(() => validateOrgId[1](req, res, next)).toThrow( - "Valid org_id must be provided", - ); - }); - }); - - describe("fetchAllRolesInOrganization", () => { - it("should fetch all roles for an existing organization", async () => { - const organizationId = "org123"; - const mockOrganization = { id: organizationId, name: "Test Org" }; - const mockRoles = [ - { id: "role1", name: "Admin", description: "Administrator" }, - { id: "role2", name: "User", description: "Regular User" }, - ]; - - organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); - organizationRoleRepositoryMock.find.mockResolvedValue(mockRoles); - - const result = - await organizationService.fetchAllRolesInOrganization(organizationId); - - expect(result).toEqual(mockRoles); - expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ - where: { id: organizationId }, - }); - expect(organizationRoleRepositoryMock.find).toHaveBeenCalledWith({ - where: { organization: { id: organizationId } }, - select: ["id", "name", "description"], - }); - }); - - it("should throw ResourceNotFound for non-existent organization", async () => { - const organizationId = "nonexistent123"; - - organizationRepositoryMock.findOne.mockResolvedValue(null); - - await expect( - organizationService.fetchAllRolesInOrganization(organizationId), - ).rejects.toThrow(ResourceNotFound); - - expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ - where: { id: organizationId }, - }); - expect(organizationRoleRepositoryMock.find).not.toHaveBeenCalled(); - }); - - it("should return an empty array when organization has no roles", async () => { - const organizationId = "org456"; - const mockOrganization = { id: organizationId, name: "Test Org" }; - - organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); - organizationRoleRepositoryMock.find.mockResolvedValue([]); - - const result = - await organizationService.fetchAllRolesInOrganization(organizationId); - - expect(result).toEqual([]); - expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ - where: { id: organizationId }, - }); - expect(organizationRoleRepositoryMock.find).toHaveBeenCalledWith({ - where: { organization: { id: organizationId } }, - select: ["id", "name", "description"], - }); - }); - }); - - describe("fetchSingleRoleInOrganisation", () => { - it("should fetch a single role", async () => { - const organisationId = "org123"; - const roleId = "role456"; - const mockOrganization: Organization = { - id: organisationId, - name: "Test Org", - }; - const mockRole: OrganizationRole = { - id: roleId, - name: "Administrator", - description: "Administrator", - permissions: [], - organization: mockOrganization, - }; - - organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); - organizationRoleRepositoryMock.findOne.mockResolvedValue(mockRole); - - const result = await organizationService.fetchSingleRole( - organisationId, - roleId, - ); - - expect(result).toEqual(mockRole); - expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ - where: { id: organisationId }, - }); - expect(organizationRoleRepositoryMock.findOne).toHaveBeenCalledWith({ - where: { id: roleId, organization: { id: organisationId } }, - relations: ["permissions"], - }); - }); - - it("should throw ResourceNotFound if the organization does not exist", async () => { - const organisationId = "nonexistent123"; - const roleId = "role456"; - - organizationRepositoryMock.findOne.mockResolvedValue(null); - - await expect( - organizationService.fetchSingleRole(organisationId, roleId), - ).rejects.toThrow(ResourceNotFound); - - expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ - where: { id: organisationId }, - }); - expect(organizationRoleRepositoryMock.findOne).not.toHaveBeenCalled(); - }); - - it("should throw ServerError for unexpected errors", async () => { - const organisationId = "org123"; - const roleId = "role456"; - const mockError = new ServerError("Database error"); - - organizationRepositoryMock.findOne.mockRejectedValue(mockError); - - await expect( - organizationService.fetchSingleRole(organisationId, roleId), - ).rejects.toThrow(ServerError); - - expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ - where: { id: organisationId }, - }); - expect(organizationRoleRepositoryMock.findOne).not.toHaveBeenCalled(); - }); - }); -}); - -describe("Update User Organization", () => { - let orgService: OrgService; - let mockRepository; - - beforeEach(() => { - mockRepository = { - findOne: jest.fn(), - update: jest.fn(), - }; - AppDataSource.getRepository.mockReturnValue(mockRepository); - orgService = new OrgService(); - }); - - it("should successfully update organization details", async () => { - const mockOrgId = "123e4567-e89b-12d3-a456-426614174000"; - const userId = "user123"; - const updateData = { - name: "New Organization Name", - email: "newemail@example.com", - industry: "Tech", - type: "Private", - country: "NGA", - address: "1234 New HNG", - state: "Lagos", - description: "A new description of the organization.", - }; - - const mockOrg = { - id: mockOrgId, - ...updateData, - }; - - mockRepository.findOne.mockResolvedValue(mockOrg); - mockRepository.update.mockResolvedValue(mockOrg); - - const result = await orgService.updateOrganizationDetails( - mockOrgId, - userId, - updateData, - ); - - expect(mockRepository.findOne).toHaveBeenCalledWith({ - where: { id: mockOrgId, userOrganizations: { user: { id: userId } } }, - }); - - expect(mockRepository.update).toHaveBeenCalledWith(mockOrgId, updateData); - expect(result).toEqual(mockOrg); - }); - - it("should throw ResourceNotFound if organization does not exist", async () => { - const mockOrgId = "123e4567-e89b-12d3-a456-426614174000"; - const userId = "user123"; - const updateData = { - name: "New Organization Name", - email: "newemail@example.com", - industry: "Tech", - type: "Private", - country: "NGA", - address: "1234 New HNG", - state: "Lagos", - description: "A new description of the organization.", - }; - - mockRepository.findOne.mockResolvedValue(null); - - await expect( - orgService.updateOrganizationDetails(mockOrgId, userId, updateData), - ).rejects.toThrow(ResourceNotFound); - - expect(mockRepository.findOne).toHaveBeenCalledWith({ - where: { id: mockOrgId, userOrganizations: { user: { id: userId } } }, - }); - - expect(mockRepository.update).not.toHaveBeenCalled(); - }); -}); +// @ts-nocheck +import jwt from "jsonwebtoken"; +import { Repository } from "typeorm"; +import { OrgController } from "../controllers"; +import AppDataSource from "../data-source"; +import { authMiddleware } from "../middleware/auth"; +import { + InvalidInput, + ResourceNotFound, + ServerError, +} from "../middleware/error"; +import { validateOrgId } from "../middleware/organizationValidation"; +import { Organization, OrganizationRole, User } from "../models"; +import { OrgService } from "../services"; + +jest.mock("../data-source", () => ({ + __esModule: true, + default: { + getRepository: jest.fn(), + initialize: jest.fn(), + isInitialized: false, + }, +})); +jest.mock("jsonwebtoken"); +jest.mock("passport", () => ({ + use: jest.fn(), +})); +jest.mock("passport-google-oauth2", () => ({ + Strategy: jest.fn(), +})); + +describe("Organization Controller and Middleware", () => { + let organizationService: OrgService; + let orgController: OrgController; + let mockManager; + let organizationRepositoryMock: jest.Mocked>; + let organizationRoleRepositoryMock: jest.Mocked>; + + beforeEach(() => { + jest.clearAllMocks(); + orgController = new OrgController(); + + organizationRepositoryMock = { + findOne: jest.fn(), + } as any; + organizationRoleRepositoryMock = { + find: jest.fn(), + findOne: jest.fn(), + } as any; + (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { + if (entity === Organization) return organizationRepositoryMock; + if (entity === OrganizationRole) return organizationRoleRepositoryMock; + return {}; + }); + + organizationService = new OrgService(); + }); + + describe("getOrganization", () => { + it("check if user is authenticated", async () => { + const req = { + headers: { + authorization: "Bearer validToken", + }, + user: undefined, + } as unknown as Request; + + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + } as unknown as Response; + + const next = jest.fn(); + + jwt.verify = jest.fn().mockImplementation((token, secret, callback) => { + callback(null, { userId: "user123" }); + }); + + User.findOne = jest.fn().mockResolvedValue({ + id: "donalTrump123", + email: "americaPresident@newyork.com", + }); + + await authMiddleware(req, res, next); + + expect(jwt.verify).toHaveBeenCalled(); + expect(User.findOne).toHaveBeenCalled(); + expect(req.user).toBeDefined(); + expect(req.user.id).toBe("donalTrump123"); + expect(next).toHaveBeenCalled(); + }); + + it("should get a single user org", async () => { + const orgId = "1"; + const orgRes = { + org_id: "1", + name: "Org 1", + description: "Org 1 description", + }; + + organizationRepositoryMock.findOne.mockResolvedValue(orgRes); + }); + + it("should pass valid UUID for org_id", async () => { + const req = { + params: { org_id: "123e4567-e89b-12d3-a456-426614174000" }, + } as unknown as Request; + const res = {} as Response; + const next = jest.fn(); + + await validateOrgId[0](req, res, next); + await validateOrgId[1](req, res, next); + + expect(next).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenCalledWith(); + }); + + it("should throw InvalidInput for empty org_id", async () => { + const req = { + params: { org_id: "" }, + } as unknown as Request; + const res = {} as Response; + const next = jest.fn(); + + await validateOrgId[0](req, res, next); + + expect(() => validateOrgId[1](req, res, next)).toThrow(InvalidInput); + expect(() => validateOrgId[1](req, res, next)).toThrow( + "Organisation id is required", + ); + }); + + it("should throw InvalidInput for non-UUID org_id", async () => { + const req = { + params: { org_id: "donald-trump-for-president" }, + } as unknown as Request; + const res = {} as Response; + const next = jest.fn(); + + await validateOrgId[0](req, res, next); + + expect(() => validateOrgId[1](req, res, next)).toThrow(InvalidInput); + expect(() => validateOrgId[1](req, res, next)).toThrow( + "Valid org_id must be provided", + ); + }); + }); + + describe("fetchAllRolesInOrganization", () => { + it("should fetch all roles for an existing organization", async () => { + const organizationId = "org123"; + const mockOrganization = { id: organizationId, name: "Test Org" }; + const mockRoles = [ + { id: "role1", name: "Admin", description: "Administrator" }, + { id: "role2", name: "User", description: "Regular User" }, + ]; + + organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); + organizationRoleRepositoryMock.find.mockResolvedValue(mockRoles); + + const result = + await organizationService.fetchAllRolesInOrganization(organizationId); + + expect(result).toEqual(mockRoles); + expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: organizationId }, + }); + expect(organizationRoleRepositoryMock.find).toHaveBeenCalledWith({ + where: { organization: { id: organizationId } }, + select: ["id", "name", "description"], + }); + }); + + it("should throw ResourceNotFound for non-existent organization", async () => { + const organizationId = "nonexistent123"; + + organizationRepositoryMock.findOne.mockResolvedValue(null); + + await expect( + organizationService.fetchAllRolesInOrganization(organizationId), + ).rejects.toThrow(ResourceNotFound); + + expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: organizationId }, + }); + expect(organizationRoleRepositoryMock.find).not.toHaveBeenCalled(); + }); + + it("should return an empty array when organization has no roles", async () => { + const organizationId = "org456"; + const mockOrganization = { id: organizationId, name: "Test Org" }; + + organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); + organizationRoleRepositoryMock.find.mockResolvedValue([]); + + const result = + await organizationService.fetchAllRolesInOrganization(organizationId); + + expect(result).toEqual([]); + expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: organizationId }, + }); + expect(organizationRoleRepositoryMock.find).toHaveBeenCalledWith({ + where: { organization: { id: organizationId } }, + select: ["id", "name", "description"], + }); + }); + }); + + describe("fetchSingleRoleInOrganisation", () => { + it("should fetch a single role", async () => { + const organisationId = "org123"; + const roleId = "role456"; + const mockOrganization: Organization = { + id: organisationId, + name: "Test Org", + }; + const mockRole: OrganizationRole = { + id: roleId, + name: "Administrator", + description: "Administrator", + permissions: [], + organization: mockOrganization, + }; + + organizationRepositoryMock.findOne.mockResolvedValue(mockOrganization); + organizationRoleRepositoryMock.findOne.mockResolvedValue(mockRole); + + const result = await organizationService.fetchSingleRole( + organisationId, + roleId, + ); + + expect(result).toEqual(mockRole); + expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: organisationId }, + }); + expect(organizationRoleRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: roleId, organization: { id: organisationId } }, + relations: ["permissions"], + }); + }); + + it("should throw ResourceNotFound if the organization does not exist", async () => { + const organisationId = "nonexistent123"; + const roleId = "role456"; + + organizationRepositoryMock.findOne.mockResolvedValue(null); + + await expect( + organizationService.fetchSingleRole(organisationId, roleId), + ).rejects.toThrow(ResourceNotFound); + + expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: organisationId }, + }); + expect(organizationRoleRepositoryMock.findOne).not.toHaveBeenCalled(); + }); + + it("should throw ServerError for unexpected errors", async () => { + const organisationId = "org123"; + const roleId = "role456"; + const mockError = new ServerError("Database error"); + + organizationRepositoryMock.findOne.mockRejectedValue(mockError); + + await expect( + organizationService.fetchSingleRole(organisationId, roleId), + ).rejects.toThrow(ServerError); + + expect(organizationRepositoryMock.findOne).toHaveBeenCalledWith({ + where: { id: organisationId }, + }); + expect(organizationRoleRepositoryMock.findOne).not.toHaveBeenCalled(); + }); + }); +}); + +describe("Update User Organization", () => { + let orgService: OrgService; + let mockRepository; + + beforeEach(() => { + mockRepository = { + findOne: jest.fn(), + update: jest.fn(), + }; + AppDataSource.getRepository.mockReturnValue(mockRepository); + orgService = new OrgService(); + }); + + it("should successfully update organization details", async () => { + const mockOrgId = "123e4567-e89b-12d3-a456-426614174000"; + const userId = "user123"; + const updateData = { + name: "New Organization Name", + email: "newemail@example.com", + industry: "Tech", + type: "Private", + country: "NGA", + address: "1234 New HNG", + state: "Lagos", + description: "A new description of the organization.", + }; + + const mockOrg = { + id: mockOrgId, + ...updateData, + }; + + mockRepository.findOne.mockResolvedValue(mockOrg); + mockRepository.update.mockResolvedValue(mockOrg); + + const result = await orgService.updateOrganizationDetails( + mockOrgId, + userId, + updateData, + ); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: mockOrgId, userOrganizations: { user: { id: userId } } }, + }); + + expect(mockRepository.update).toHaveBeenCalledWith(mockOrgId, updateData); + expect(result).toEqual(mockOrg); + }); + + it("should throw ResourceNotFound if organization does not exist", async () => { + const mockOrgId = "123e4567-e89b-12d3-a456-426614174000"; + const userId = "user123"; + const updateData = { + name: "New Organization Name", + email: "newemail@example.com", + industry: "Tech", + type: "Private", + country: "NGA", + address: "1234 New HNG", + state: "Lagos", + description: "A new description of the organization.", + }; + + mockRepository.findOne.mockResolvedValue(null); + + await expect( + orgService.updateOrganizationDetails(mockOrgId, userId, updateData), + ).rejects.toThrow(ResourceNotFound); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: mockOrgId, userOrganizations: { user: { id: userId } } }, + }); + + expect(mockRepository.update).not.toHaveBeenCalled(); + }); +}); diff --git a/src/test/product.spec.ts b/src/test/product.spec.ts index 2baf0ad1..a93d53c1 100644 --- a/src/test/product.spec.ts +++ b/src/test/product.spec.ts @@ -1,379 +1,429 @@ -//@ts-nocheck -import { ProductService } from "../services/product.services"; -import { Repository } from "typeorm"; -import { Product } from "../models/product"; -import { StockStatus } from "../enums/product"; -import { ProductSchema } from "../schema/product.schema"; -import { Organization } from "../models/organization"; -import { ServerError, ResourceNotFound } from "../middleware"; - -jest.mock("../utils", () => ({ - getIsInvalidMessage: jest - .fn() - .mockImplementation((field: string) => `${field} is invalid`), -})); - -jest.mock("../data-source", () => ({ - getRepository: jest.fn().mockImplementation((entity) => ({ - create: jest.fn().mockReturnValue({}), - save: jest.fn(), - findOne: jest.fn(), - remove: jest.fn(), - createQueryBuilder: jest.fn(() => ({ - where: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - take: jest.fn().mockReturnThis(), - getManyAndCount: jest.fn().mockReturnValue([[], 0]), - })), - })), -})); - -describe("ProductService", () => { - let productService: ProductService; - let productRepository: Repository; - let organizationRepository: Repository; - - beforeEach(() => { - productService = new ProductService(); - productRepository = productService["productRepository"]; - organizationRepository = productService["organizationRepository"]; - }); - - describe("createProducts", () => { - it("should create a product successfully", async () => { - // Mock data - const mockOrgId = "1"; - const mockProduct: ProductSchema = { - name: "Test Product", - description: "This is a test product", - price: 50, - quantity: 50, - }; - const mockOrganization = { id: "1", name: "Test Organization" }; - const mockCreatedProduct = { - ...mockProduct, - id: "1", - stock_status: StockStatus.IN_STOCK, - }; - - productService["checkEntities"] = jest - .fn() - .mockResolvedValue({ organization: mockOrganization }); - productRepository.create = jest.fn().mockReturnValue(mockCreatedProduct); - productRepository.save = jest.fn().mockResolvedValue(mockCreatedProduct); - organizationRepository.findOne = jest - .fn() - .mockResolvedValue(mockOrganization); - productService["calculateProductStatus"] = jest - .fn() - .mockResolvedValue(StockStatus.IN_STOCK); - - const result = await productService.createProduct(mockOrgId, mockProduct); - expect(result.status_code).toEqual(201); - expect(result.status).toEqual("success"); - expect(result.message).toEqual("Product created successfully"); - expect(result.data.name).toEqual(mockProduct.name); - expect(result.data.description).toEqual(mockProduct.description); - expect(result.data.price).toEqual(mockProduct.price); - expect(result.data.quantity).toEqual(mockProduct.quantity); - expect(result.data.status).toEqual(StockStatus.IN_STOCK); - }); - - it("should throw an error when credentials are invalid", async () => { - const mockOrgId = "nonexistentOrg"; - const mockProduct: ProductSchema = { - name: "Test Product", - description: "This is a test product", - price: 50, - quantity: 10, - }; - - productService["checkEntities"] = jest - .fn() - .mockResolvedValue({ organization: undefined }); - await expect( - productService.createProduct(mockOrgId, mockProduct), - ).rejects.toThrow(ServerError); - }); - - it("should throw an error when product data is invalid", async () => { - const mockOrgId = "1"; - const invalidProduct: ProductSchema = { - name: "", - description: "This is a test product", - price: -10, - quantity: 50, - }; - const mockOrganization = { id: "1", name: "Test Organization" }; - - productService["checkEntities"] = jest - .fn() - .mockResolvedValue({ organization: mockOrganization }); - - await expect( - productService.createProduct(mockOrgId, invalidProduct), - ).rejects.toThrow(Error); - }); - - it("should throw a server error when product creation fails", async () => { - const mockOrgId = "1"; - const mockProduct: ProductSchema = { - name: "Test Product", - description: "This is a test product", - price: 50, - quantity: 10, - }; - - productService["checkEntities"] = jest.fn().mockResolvedValue({}); - productRepository.create = jest.fn().mockReturnValue(mockProduct); - productRepository.save = jest - .fn() - .mockRejectedValue(new Error("Database error")); - - await expect( - productService.createProduct(mockOrgId, mockProduct), - ).rejects.toThrow(ServerError); - }); - }); - - describe("getProducts", () => { - it("should search products successfully", async () => { - const mockOrgId = "1"; - const mockQuery = { name: "Test", minPrice: 0, maxPrice: 100 }; - const mockOrg = { id: "1", name: "Test Organization" }; - const mockProducts = [{ id: "1", name: "Test Product", price: 50 }]; - const mockTotalCount = 2; - - const mockQueryBuilder = { - where: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - take: jest.fn().mockReturnThis(), - getManyAndCount: jest - .fn() - .mockResolvedValue([mockProducts, mockTotalCount]), - }; - - productRepository.createQueryBuilder = jest - .fn() - .mockReturnValue(mockQueryBuilder); - organizationRepository.findOne = jest.fn().mockResolvedValue(mockOrg); - - const result = await productService.getProducts(mockOrgId, mockQuery); - expect(result.success).toBe(true); - expect(result.statusCode).toBe(200); - expect(result.data.products).toEqual(mockProducts); - expect(result.data.pagination.total).toBe(mockTotalCount); - expect(result.data.pagination.page).toBe(1); - expect(result.data.pagination.limit).toBe(10); - }); - it("should search products successfully with specified pagination", async () => { - const mockOrgId = "1"; - const mockQuery = { name: "Test", minPrice: 0, maxPrice: 100 }; - const mockPage = 2; - const mockLimit = 5; - const mockOrg = { id: "1", name: "Test Organization" }; - const mockProducts = [{ id: "1", name: "Test Product", price: 50 }]; - const mockTotalCount = 5; - - const mockQueryBuilder = { - where: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - take: jest.fn().mockReturnThis(), - getManyAndCount: jest - .fn() - .mockResolvedValue([mockProducts, mockTotalCount]), - }; - - productRepository.createQueryBuilder = jest - .fn() - .mockReturnValue(mockQueryBuilder); - organizationRepository.findOne = jest.fn().mockResolvedValue(mockOrg); - - const result = await productService.getProducts( - mockOrgId, - mockQuery, - mockPage, - mockLimit, - ); - expect(result.success).toBe(true); - expect(result.statusCode).toBe(200); - expect(result.data.pagination.total).toBe(mockTotalCount); - expect(result.data.pagination.page).toBe(mockPage); - expect(result.data.pagination.limit).toBe(mockLimit); - }); - - it("should return an empty product array when product is not found", async () => { - const mockOrgId = "1"; - const mockQuery = { name: "Nonexistent Product" }; - const mockOrg = { id: "1", name: "Test Organization" }; - const mockTotalCount = 0; - const mockProducts = []; - - const mockQueryBuilder = { - where: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - take: jest.fn().mockReturnThis(), - getManyAndCount: jest.fn().mockResolvedValue([[], 0]), - }; - - productRepository.createQueryBuilder = jest - .fn() - .mockReturnValue(mockQueryBuilder); - organizationRepository.findOne = jest.fn().mockResolvedValue(mockOrg); - - const result = await productService.getProducts(mockOrgId, mockQuery); - expect(result.success).toBe(true); - expect(result.statusCode).toBe(200); - expect(result.data.pagination.total).toBe(mockTotalCount); - expect(result.data.products).toStrictEqual(mockProducts); - }); - - it("should throw a ServerError when organization is not found", async () => { - const mockOrgId = "nonexistentOrg"; - const mockQuery = { name: "Test Product" }; - - organizationRepository.findOne = jest.fn().mockResolvedValue(undefined); - - await expect( - productService.getProducts(mockOrgId, mockQuery), - ).rejects.toThrow(ServerError); - }); - - it("should throw a server error when organization is not found", async () => { - const mockOrgId = "nonexistentOrg"; - const mockQuery = { name: "Test Product" }; - organizationRepository.findOne = jest.fn().mockResolvedValue(undefined); - await expect( - productService.getProducts(mockOrgId, mockQuery), - ).rejects.toThrow(ServerError); - }); - }); - - describe("deleteProduct", () => { - it("should delete the product from the organization", async () => { - const org_id = "org123"; - const product_id = "prod123"; - // Mock data - const mockProduct = { id: product_id, name: "Test Product" } as Product; - productService["checkEntities"] = jest - .fn() - .mockResolvedValue({ product: mockProduct }); - productRepository.remove = jest.fn().mockResolvedValue(mockProduct); - - await productService.deleteProduct(org_id, product_id); - - // Verify that the checkEntities and remove methods were called with the correct parameters - expect(productService["checkEntities"]).toHaveBeenCalledWith({ - organization: org_id, - product: product_id, - }); - expect(productRepository.remove).toHaveBeenCalledWith(mockProduct); - }); - it("should throw an error if the product is not found", async () => { - const org_id = "org123"; - const product_id = "prod123"; - - // Mock the checkEntities method to return undefined for product - productService["checkEntities"] = jest - .fn() - .mockResolvedValue({ product: undefined }); - - await expect( - productService.deleteProduct(org_id, product_id), - ).rejects.toThrow("Product not found"); - - // Verify that the checkEntities method was called correctly and remove method was not called - expect(productService["checkEntities"]).toHaveBeenCalledWith({ - organization: org_id, - product: product_id, - }); - expect(productRepository.remove).not.toHaveBeenCalled(); - }); - - it("should throw an error if checkEntities fails", async () => { - const org_id = "org123"; - const product_id = "prod123"; - - // Mock the checkEntities method to throw an error - productService["checkEntities"] = jest - .fn() - .mockRejectedValue(new Error("Check entities failed")); - - await expect( - productService.deleteProduct(org_id, product_id), - ).rejects.toThrow("Failed to delete product: Check entities failed"); - - // Verify that the checkEntities method was called correctly and remove method was not called - expect(productService["checkEntities"]).toHaveBeenCalledWith({ - organization: org_id, - product: product_id, - }); - expect(productRepository.remove).not.toHaveBeenCalled(); - }); - }); - describe("get single Product", () => { - it("should get the product from the organization", async () => { - const org_id = "org123"; - const product_id = "prod123"; - // Mock data - const mockProduct = { id: product_id, name: "Test Product" } as Product; - - jest - .spyOn(productService, "checkEntities") - .mockResolvedValue({ product: mockProduct }); - - const product = await productService.getProduct(org_id, product_id); - - expect(productService["checkEntities"]).toHaveBeenCalledWith({ - organization: org_id, - product: product_id, - }); - expect(product).toEqual(mockProduct); - }); - it("should throw an error if the product is not found", async () => { - const org_id = "org123"; - const product_id = "prod123"; - - // Mock the checkEntities method to return undefined for product - productService["checkEntities"] = jest - .fn() - .mockResolvedValue({ product: undefined }); - - await expect( - productService.deleteProduct(org_id, product_id), - ).rejects.toThrow("Product not found"); - - // Verify that the checkEntities method was called correctly and remove method was not called - expect(productService["checkEntities"]).toHaveBeenCalledWith({ - organization: org_id, - product: product_id, - }); - expect(productRepository.findOne).not.toHaveBeenCalled(); - }); - - it("should throw an error if checkEntities fails", async () => { - const org_id = "org123"; - const product_id = "prod123"; - - // Mock the checkEntities method to throw an error - productService["checkEntities"] = jest - .fn() - .mockRejectedValue(new Error("Check entities failed")); - - await expect( - productService.getProduct(org_id, product_id), - ).rejects.toThrow("Check entities failed"); - - // Verify that the checkEntities method was called correctly and remove method was not called - expect(productService["checkEntities"]).toHaveBeenCalledWith({ - organization: org_id, - product: product_id, - }); - expect(productRepository.findOne).not.toHaveBeenCalled(); - }); - }); -}); +//@ts-nocheck +import { ProductService } from "../services/product.services"; +import { Repository } from "typeorm"; +import { Product } from "../models/product"; +import { StockStatus } from "../enums/product"; +import { ProductSchema } from "../schema/product.schema"; +import { Organization } from "../models/organization"; +import { ServerError, ResourceNotFound } from "../middleware"; + +jest.mock("../utils", () => ({ + getIsInvalidMessage: jest + .fn() + .mockImplementation((field: string) => `${field} is invalid`), +})); + +jest.mock("../data-source", () => ({ + getRepository: jest.fn().mockImplementation((entity) => ({ + create: jest.fn().mockReturnValue({}), + save: jest.fn(), + findOne: jest.fn(), + remove: jest.fn(), + createQueryBuilder: jest.fn(() => ({ + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + getManyAndCount: jest.fn().mockReturnValue([[], 0]), + })), + })), +})); + +describe("ProductService", () => { + let productService: ProductService; + let productRepository: Repository; + let organizationRepository: Repository; + + beforeEach(() => { + productService = new ProductService(); + productRepository = productService["productRepository"]; + organizationRepository = productService["organizationRepository"]; + }); + + describe("createProducts", () => { + it("should create a product successfully", async () => { + // Mock data + const mockOrgId = "1"; + const mockProduct: ProductSchema = { + name: "Test Product", + description: "This is a test product", + price: 50, + quantity: 50, + }; + const mockOrganization = { id: "1", name: "Test Organization" }; + const mockCreatedProduct = { + ...mockProduct, + id: "1", + stock_status: StockStatus.IN_STOCK, + }; + + productService["checkEntities"] = jest + .fn() + .mockResolvedValue({ organization: mockOrganization }); + productRepository.create = jest.fn().mockReturnValue(mockCreatedProduct); + productRepository.save = jest.fn().mockResolvedValue(mockCreatedProduct); + organizationRepository.findOne = jest + .fn() + .mockResolvedValue(mockOrganization); + productService["calculateProductStatus"] = jest + .fn() + .mockResolvedValue(StockStatus.IN_STOCK); + + const result = await productService.createProduct(mockOrgId, mockProduct); + expect(result.status_code).toEqual(201); + expect(result.status).toEqual("success"); + expect(result.message).toEqual("Product created successfully"); + expect(result.data.name).toEqual(mockProduct.name); + expect(result.data.description).toEqual(mockProduct.description); + expect(result.data.price).toEqual(mockProduct.price); + expect(result.data.quantity).toEqual(mockProduct.quantity); + expect(result.data.status).toEqual(StockStatus.IN_STOCK); + }); + + it("should throw an error when credentials are invalid", async () => { + const mockOrgId = "nonexistentOrg"; + const mockProduct: ProductSchema = { + name: "Test Product", + description: "This is a test product", + price: 50, + quantity: 10, + }; + + productService["checkEntities"] = jest + .fn() + .mockResolvedValue({ organization: undefined }); + await expect( + productService.createProduct(mockOrgId, mockProduct), + ).rejects.toThrow(ServerError); + }); + + it("should throw an error when product data is invalid", async () => { + const mockOrgId = "1"; + const invalidProduct: ProductSchema = { + name: "", + description: "This is a test product", + price: -10, + quantity: 50, + }; + const mockOrganization = { id: "1", name: "Test Organization" }; + + productService["checkEntities"] = jest + .fn() + .mockResolvedValue({ organization: mockOrganization }); + + await expect( + productService.createProduct(mockOrgId, invalidProduct), + ).rejects.toThrow(Error); + }); + + it("should throw a server error when product creation fails", async () => { + const mockOrgId = "1"; + const mockProduct: ProductSchema = { + name: "Test Product", + description: "This is a test product", + price: 50, + quantity: 10, + }; + + productService["checkEntities"] = jest.fn().mockResolvedValue({}); + productRepository.create = jest.fn().mockReturnValue(mockProduct); + productRepository.save = jest + .fn() + .mockRejectedValue(new Error("Database error")); + + await expect( + productService.createProduct(mockOrgId, mockProduct), + ).rejects.toThrow(ServerError); + }); + }); + + describe("getProducts", () => { + it("should search products successfully", async () => { + const mockOrgId = "1"; + const mockQuery = { name: "Test", minPrice: 0, maxPrice: 100 }; + const mockOrg = { id: "1", name: "Test Organization" }; + const mockProducts = [{ id: "1", name: "Test Product", price: 50 }]; + const mockTotalCount = 2; + + const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + getManyAndCount: jest + .fn() + .mockResolvedValue([mockProducts, mockTotalCount]), + }; + + productRepository.createQueryBuilder = jest + .fn() + .mockReturnValue(mockQueryBuilder); + organizationRepository.findOne = jest.fn().mockResolvedValue(mockOrg); + + const result = await productService.getProducts(mockOrgId, mockQuery); + expect(result.success).toBe(true); + expect(result.statusCode).toBe(200); + expect(result.data.products).toEqual(mockProducts); + expect(result.data.pagination.total).toBe(mockTotalCount); + expect(result.data.pagination.page).toBe(1); + expect(result.data.pagination.limit).toBe(10); + }); + it("should search products successfully with specified pagination", async () => { + const mockOrgId = "1"; + const mockQuery = { name: "Test", minPrice: 0, maxPrice: 100 }; + const mockPage = 2; + const mockLimit = 5; + const mockOrg = { id: "1", name: "Test Organization" }; + const mockProducts = [{ id: "1", name: "Test Product", price: 50 }]; + const mockTotalCount = 5; + + const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + getManyAndCount: jest + .fn() + .mockResolvedValue([mockProducts, mockTotalCount]), + }; + + productRepository.createQueryBuilder = jest + .fn() + .mockReturnValue(mockQueryBuilder); + organizationRepository.findOne = jest.fn().mockResolvedValue(mockOrg); + + const result = await productService.getProducts( + mockOrgId, + mockQuery, + mockPage, + mockLimit, + ); + expect(result.success).toBe(true); + expect(result.statusCode).toBe(200); + expect(result.data.pagination.total).toBe(mockTotalCount); + expect(result.data.pagination.page).toBe(mockPage); + expect(result.data.pagination.limit).toBe(mockLimit); + }); + + it("should return an empty product array when product is not found", async () => { + const mockOrgId = "1"; + const mockQuery = { name: "Nonexistent Product" }; + const mockOrg = { id: "1", name: "Test Organization" }; + const mockTotalCount = 0; + const mockProducts = []; + + const mockQueryBuilder = { + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + getManyAndCount: jest.fn().mockResolvedValue([[], 0]), + }; + + productRepository.createQueryBuilder = jest + .fn() + .mockReturnValue(mockQueryBuilder); + organizationRepository.findOne = jest.fn().mockResolvedValue(mockOrg); + + const result = await productService.getProducts(mockOrgId, mockQuery); + expect(result.success).toBe(true); + expect(result.statusCode).toBe(200); + expect(result.data.pagination.total).toBe(mockTotalCount); + expect(result.data.products).toStrictEqual(mockProducts); + }); + + it("should throw a ServerError when organization is not found", async () => { + const mockOrgId = "nonexistentOrg"; + const mockQuery = { name: "Test Product" }; + + organizationRepository.findOne = jest.fn().mockResolvedValue(undefined); + + await expect( + productService.getProducts(mockOrgId, mockQuery), + ).rejects.toThrow(ServerError); + }); + + it("should throw a server error when organization is not found", async () => { + const mockOrgId = "nonexistentOrg"; + const mockQuery = { name: "Test Product" }; + organizationRepository.findOne = jest.fn().mockResolvedValue(undefined); + await expect( + productService.getProducts(mockOrgId, mockQuery), + ).rejects.toThrow(ServerError); + }); + }); + + describe("deleteProduct", () => { + it("should delete the product from the organization", async () => { + const org_id = "org123"; + const product_id = "prod123"; + // Mock data + const mockProduct = { id: product_id, name: "Test Product" } as Product; + productService["checkEntities"] = jest + .fn() + .mockResolvedValue({ product: mockProduct }); + productRepository.remove = jest.fn().mockResolvedValue(mockProduct); + + await productService.deleteProduct(org_id, product_id); + + // Verify that the checkEntities and remove methods were called with the correct parameters + expect(productService["checkEntities"]).toHaveBeenCalledWith({ + organization: org_id, + product: product_id, + }); + expect(productRepository.remove).toHaveBeenCalledWith(mockProduct); + }); + it("should throw an error if the product is not found", async () => { + const org_id = "org123"; + const product_id = "prod123"; + + // Mock the checkEntities method to return undefined for product + productService["checkEntities"] = jest + .fn() + .mockResolvedValue({ product: undefined }); + + await expect( + productService.deleteProduct(org_id, product_id), + ).rejects.toThrow("Product not found"); + + // Verify that the checkEntities method was called correctly and remove method was not called + expect(productService["checkEntities"]).toHaveBeenCalledWith({ + organization: org_id, + product: product_id, + }); + expect(productRepository.remove).not.toHaveBeenCalled(); + }); + + it("should throw an error if checkEntities fails", async () => { + const org_id = "org123"; + const product_id = "prod123"; + + // Mock the checkEntities method to throw an error + productService["checkEntities"] = jest + .fn() + .mockRejectedValue(new Error("Check entities failed")); + + await expect( + productService.deleteProduct(org_id, product_id), + ).rejects.toThrow("Failed to delete product: Check entities failed"); + + // Verify that the checkEntities method was called correctly and remove method was not called + expect(productService["checkEntities"]).toHaveBeenCalledWith({ + organization: org_id, + product: product_id, + }); + expect(productRepository.remove).not.toHaveBeenCalled(); + }); + }); + describe("get single Product", () => { + it("should get the product from the organization", async () => { + const org_id = "org123"; + const product_id = "prod123"; + // Mock data + const mockProduct = { id: product_id, name: "Test Product" } as Product; + + jest + .spyOn(productService, "checkEntities") + .mockResolvedValue({ product: mockProduct }); + + const product = await productService.getProduct(org_id, product_id); + + expect(productService["checkEntities"]).toHaveBeenCalledWith({ + organization: org_id, + product: product_id, + }); + expect(product).toEqual(mockProduct); + }); + it("should throw an error if the product is not found", async () => { + const org_id = "org123"; + const product_id = "prod123"; + + // Mock the checkEntities method to return undefined for product + productService["checkEntities"] = jest + .fn() + .mockResolvedValue({ product: undefined }); + + await expect( + productService.deleteProduct(org_id, product_id), + ).rejects.toThrow("Product not found"); + + // Verify that the checkEntities method was called correctly and remove method was not called + expect(productService["checkEntities"]).toHaveBeenCalledWith({ + organization: org_id, + product: product_id, + }); + expect(productRepository.findOne).not.toHaveBeenCalled(); + }); + + it("should throw an error if checkEntities fails", async () => { + const org_id = "org123"; + const product_id = "prod123"; + + // Mock the checkEntities method to throw an error + productService["checkEntities"] = jest + .fn() + .mockRejectedValue(new Error("Check entities failed")); + + await expect( + productService.getProduct(org_id, product_id), + ).rejects.toThrow("Check entities failed"); + + // Verify that the checkEntities method was called correctly and remove method was not called + expect(productService["checkEntities"]).toHaveBeenCalledWith({ + organization: org_id, + product: product_id, + }); + expect(productRepository.findOne).not.toHaveBeenCalled(); + }); + }); + describe("updateProduct", () => { + it("should successfully update a product", async () => { + const org_id = "org123"; + const product_id = "prod123"; + const mockProduct = { id: product_id, name: "Test Product" } as Product; + const updateDetails = { price: 10 }; + const updatedProduct = { ...mockProduct, ...updateDetails }; + + // Mocking checkEntities to return the product + productService["checkEntities"] = jest.fn().mockResolvedValue({ + product: mockProduct, + }); + + // Mocking the save method to return the updated product + productRepository.save = jest.fn().mockResolvedValue(updatedProduct); + + const result = await productService.updateProduct( + org_id, + product_id, + updateDetails, + ); + + expect(productRepository.save).toHaveBeenCalledWith(updatedProduct); + expect(result).toEqual(updatedProduct); + }); + + it("should throw ServerError if product update fails", async () => { + const org_id = "org123"; + const product_id = "prod123"; + const mockProduct = { id: product_id, name: "Test Product" } as Product; + const updateDetails = { price: 10 }; + + // Mocking checkEntities to return the product + productService["checkEntities"] = jest.fn().mockResolvedValue({ + product: mockProduct, + }); + + // Mocking the save method to return undefined (simulating a failure) + productRepository.save = jest.fn().mockResolvedValue(undefined); + + await expect( + productService.updateProduct(org_id, product_id, updateDetails), + ).rejects.toThrow(ServerError); + + expect(productRepository.save).toHaveBeenCalledWith({ + ...mockProduct, + ...updateDetails, + }); + }); + }) +}); \ No newline at end of file diff --git a/src/test/roles.spec.ts b/src/test/roles.spec.ts index f8646f8f..bdfcc650 100644 --- a/src/test/roles.spec.ts +++ b/src/test/roles.spec.ts @@ -1,60 +1,60 @@ -import { createRole } from "../services/role.services"; -import { User } from "../models"; -import { UserRole } from "../enums/userRoles"; -import { HttpError } from "../middleware/error"; - -jest.mock("../models"); - -describe("Role Service", () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("createRole", () => { - it("should update user role successfully", async () => { - const mockUser = { - id: "123", - role: UserRole.USER, - save: jest.fn(), - }; - (User.findOne as jest.Mock).mockResolvedValue(mockUser); - - const result = await createRole("123", UserRole.ADMIN); - - expect(User.findOne).toHaveBeenCalledWith({ where: { id: "123" } }); - expect(mockUser.role).toBe(UserRole.ADMIN); - expect(mockUser.save).toHaveBeenCalled(); - expect(result).toEqual(mockUser); - }); - - it("should throw HttpError if user is not found", async () => { - (User.findOne as jest.Mock).mockResolvedValue(null); - - await expect(createRole("123", UserRole.ADMIN)).rejects.toThrow( - new HttpError(404, "User not found"), - ); - }); - - it("should throw HttpError if invalid role is specified", async () => { - const mockUser = { - id: "123", - role: UserRole.USER, - }; - (User.findOne as jest.Mock).mockResolvedValue(mockUser); - - await expect( - createRole("123", "INVALID_ROLE" as UserRole), - ).rejects.toThrow(new HttpError(400, "Invalid role specified")); - }); - - it("should database errors", async () => { - (User.findOne as jest.Mock).mockRejectedValue( - new Error("Database error"), - ); - - await expect(createRole("123", UserRole.ADMIN)).rejects.toThrow( - "Database error", - ); - }); - }); -}); +import { createRole } from "../services/role.services"; +import { User } from "../models"; +import { UserRole } from "../enums/userRoles"; +import { HttpError } from "../middleware/error"; + +jest.mock("../models"); + +describe("Role Service", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("createRole", () => { + it("should update user role successfully", async () => { + const mockUser = { + id: "123", + role: UserRole.USER, + save: jest.fn(), + }; + (User.findOne as jest.Mock).mockResolvedValue(mockUser); + + const result = await createRole("123", UserRole.ADMIN); + + expect(User.findOne).toHaveBeenCalledWith({ where: { id: "123" } }); + expect(mockUser.role).toBe(UserRole.ADMIN); + expect(mockUser.save).toHaveBeenCalled(); + expect(result).toEqual(mockUser); + }); + + it("should throw HttpError if user is not found", async () => { + (User.findOne as jest.Mock).mockResolvedValue(null); + + await expect(createRole("123", UserRole.ADMIN)).rejects.toThrow( + new HttpError(404, "User not found"), + ); + }); + + it("should throw HttpError if invalid role is specified", async () => { + const mockUser = { + id: "123", + role: UserRole.USER, + }; + (User.findOne as jest.Mock).mockResolvedValue(mockUser); + + await expect( + createRole("123", "INVALID_ROLE" as UserRole), + ).rejects.toThrow(new HttpError(400, "Invalid role specified")); + }); + + it("should database errors", async () => { + (User.findOne as jest.Mock).mockRejectedValue( + new Error("Database error"), + ); + + await expect(createRole("123", UserRole.ADMIN)).rejects.toThrow( + "Database error", + ); + }); + }); +}); diff --git a/src/test/superadmin.spec.ts b/src/test/superadmin.spec.ts index ec865c72..dd895fe7 100644 --- a/src/test/superadmin.spec.ts +++ b/src/test/superadmin.spec.ts @@ -1,173 +1,173 @@ -// @ts-nocheck -import { Request } from "express"; -import { User } from "../models/user"; // Ensure this matches the actual file casing -import AppDataSource from "../data-source"; -import { AdminUserService } from "../services/admin.services"; -import { HttpError } from "../middleware"; -import { hashPassword } from "../utils/index"; - -jest.mock("../data-source"); -jest.mock("../utils/index"); - -describe("AdminUserService", () => { - let adminUserService: AdminUserService; - let consoleErrorMock: jest.SpyInstance; - - beforeAll(() => { - consoleErrorMock = jest - .spyOn(console, "error") - .mockImplementation(() => {}); - }); - - afterAll(() => { - consoleErrorMock.mockRestore(); - }); - - beforeEach(() => { - adminUserService = new AdminUserService(); - }); - - describe("updateUser", () => { - it("should update the user successfully", async () => { - const req = { - body: { - firstName: "New", - lastName: "Name", - email: "existinguser@example.com", - role: "admin", - password: "newPassword", - isverified: true, - }, - params: { id: "1" }, - } as unknown as Request; - - const mockUser = { - id: "1", - name: "Existing User", - email: "existinguser@example.com", - password: "oldPasswordHash", - isverified: false, - createdAt: new Date(), - updatedAt: new Date(), - } as User; - - const updatedFields = { - name: "New Name", - email: "existinguser@example.com", - role: "admin", - password: "newPasswordHash", - isverified: true, - }; - - const mockUpdatedUser = { - ...mockUser, - ...updatedFields, - updatedAt: new Date(), - }; - - const userRepository = { - findOne: jest.fn().mockResolvedValue(mockUser), - update: jest.fn().mockImplementation((id, fields) => { - Object.assign(mockUser, fields); - return Promise.resolve(); - }), - findOneBy: jest.fn().mockResolvedValue(mockUpdatedUser), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue( - userRepository, - ); - (hashPassword as jest.Mock).mockResolvedValue("newPasswordHash"); - - const result = await adminUserService.updateUser(req); - - expect(userRepository.findOne).toHaveBeenCalledWith({ - where: { email: "existinguser@example.com" }, - }); - expect(userRepository.update).toHaveBeenCalledWith("1", { - name: "New Name", - email: "existinguser@example.com", - role: "admin", - password: "newPasswordHash", - isverified: true, - }); - // expect(result).toEqual(mockUpdatedUser); - expect(result.id).toEqual(mockUpdatedUser.id); - expect(result.name).toEqual(mockUpdatedUser.name); - expect(result.email).toEqual(mockUpdatedUser.email); - expect(result.role).toEqual(mockUpdatedUser.role); - expect(result.password).toEqual(mockUpdatedUser.password); - expect(result.isverified).toEqual(mockUpdatedUser.isverified); - }); - - it("should throw a 404 error if user is not found", async () => { - const req = { - body: { - firstName: "New", - lastName: "Name", - email: "nonexistentuser@example.com", - role: "admin", - password: "newPassword", - isverified: true, - }, - params: { id: "1" }, - } as unknown as Request; - - const userRepository = { - findOne: jest.fn().mockResolvedValue(null), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue( - userRepository, - ); - - await expect(adminUserService.updateUser(req)).rejects.toThrow(HttpError); - await expect(adminUserService.updateUser(req)).rejects.toThrow( - "User not found", - ); - expect(userRepository.findOne).toHaveBeenCalledWith({ - where: { email: "nonexistentuser@example.com" }, - }); - }); - - it("should throw an error if hashing the password fails", async () => { - const req = { - body: { - firstName: "New", - lastName: "Name", - email: "existinguser@example.com", - role: "admin", - password: "newPassword", - isverified: true, - }, - params: { id: "1" }, - } as unknown as Request; - - const mockUser = { - id: "1", - name: "Existing User", - email: "existinguser@example.com", - password: "oldPasswordHash", - isverified: false, - createdAt: new Date(), - updatedAt: new Date(), - } as User; - - const userRepository = { - findOne: jest.fn().mockResolvedValue(mockUser), - update: jest.fn(), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue( - userRepository, - ); - (hashPassword as jest.Mock).mockRejectedValue( - new Error("Hashing failed"), - ); - - await expect(adminUserService.updateUser(req)).rejects.toThrow( - "Hashing failed", - ); - }); - }); -}); +// @ts-nocheck +import { Request } from "express"; +import { User } from "../models/user"; // Ensure this matches the actual file casing +import AppDataSource from "../data-source"; +import { AdminUserService } from "../services/admin.services"; +import { HttpError } from "../middleware"; +import { hashPassword } from "../utils/index"; + +jest.mock("../data-source"); +jest.mock("../utils/index"); + +describe("AdminUserService", () => { + let adminUserService: AdminUserService; + let consoleErrorMock: jest.SpyInstance; + + beforeAll(() => { + consoleErrorMock = jest + .spyOn(console, "error") + .mockImplementation(() => {}); + }); + + afterAll(() => { + consoleErrorMock.mockRestore(); + }); + + beforeEach(() => { + adminUserService = new AdminUserService(); + }); + + describe("updateUser", () => { + it("should update the user successfully", async () => { + const req = { + body: { + firstName: "New", + lastName: "Name", + email: "existinguser@example.com", + role: "admin", + password: "newPassword", + isverified: true, + }, + params: { id: "1" }, + } as unknown as Request; + + const mockUser = { + id: "1", + name: "Existing User", + email: "existinguser@example.com", + password: "oldPasswordHash", + isverified: false, + createdAt: new Date(), + updatedAt: new Date(), + } as User; + + const updatedFields = { + name: "New Name", + email: "existinguser@example.com", + role: "admin", + password: "newPasswordHash", + isverified: true, + }; + + const mockUpdatedUser = { + ...mockUser, + ...updatedFields, + updatedAt: new Date(), + }; + + const userRepository = { + findOne: jest.fn().mockResolvedValue(mockUser), + update: jest.fn().mockImplementation((id, fields) => { + Object.assign(mockUser, fields); + return Promise.resolve(); + }), + findOneBy: jest.fn().mockResolvedValue(mockUpdatedUser), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue( + userRepository, + ); + (hashPassword as jest.Mock).mockResolvedValue("newPasswordHash"); + + const result = await adminUserService.updateUser(req); + + expect(userRepository.findOne).toHaveBeenCalledWith({ + where: { email: "existinguser@example.com" }, + }); + expect(userRepository.update).toHaveBeenCalledWith("1", { + name: "New Name", + email: "existinguser@example.com", + role: "admin", + password: "newPasswordHash", + isverified: true, + }); + // expect(result).toEqual(mockUpdatedUser); + expect(result.id).toEqual(mockUpdatedUser.id); + expect(result.name).toEqual(mockUpdatedUser.name); + expect(result.email).toEqual(mockUpdatedUser.email); + expect(result.role).toEqual(mockUpdatedUser.role); + expect(result.password).toEqual(mockUpdatedUser.password); + expect(result.isverified).toEqual(mockUpdatedUser.isverified); + }); + + it("should throw a 404 error if user is not found", async () => { + const req = { + body: { + firstName: "New", + lastName: "Name", + email: "nonexistentuser@example.com", + role: "admin", + password: "newPassword", + isverified: true, + }, + params: { id: "1" }, + } as unknown as Request; + + const userRepository = { + findOne: jest.fn().mockResolvedValue(null), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue( + userRepository, + ); + + await expect(adminUserService.updateUser(req)).rejects.toThrow(HttpError); + await expect(adminUserService.updateUser(req)).rejects.toThrow( + "User not found", + ); + expect(userRepository.findOne).toHaveBeenCalledWith({ + where: { email: "nonexistentuser@example.com" }, + }); + }); + + it("should throw an error if hashing the password fails", async () => { + const req = { + body: { + firstName: "New", + lastName: "Name", + email: "existinguser@example.com", + role: "admin", + password: "newPassword", + isverified: true, + }, + params: { id: "1" }, + } as unknown as Request; + + const mockUser = { + id: "1", + name: "Existing User", + email: "existinguser@example.com", + password: "oldPasswordHash", + isverified: false, + createdAt: new Date(), + updatedAt: new Date(), + } as User; + + const userRepository = { + findOne: jest.fn().mockResolvedValue(mockUser), + update: jest.fn(), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue( + userRepository, + ); + (hashPassword as jest.Mock).mockRejectedValue( + new Error("Hashing failed"), + ); + + await expect(adminUserService.updateUser(req)).rejects.toThrow( + "Hashing failed", + ); + }); + }); +}); diff --git a/src/test/superadminDeleteOrg.spec.ts b/src/test/superadminDeleteOrg.spec.ts index dc132de3..0b7dcdef 100644 --- a/src/test/superadminDeleteOrg.spec.ts +++ b/src/test/superadminDeleteOrg.spec.ts @@ -1,102 +1,102 @@ -// @ts-nocheck -import AppDataSource from "../data-source"; -import { HttpError } from "../middleware"; -import { AdminOrganisationService } from "../services/admin.services"; - -jest.mock("../data-source"); -jest.mock("../utils/index"); - -describe("AdminUserService", () => { - let consoleErrorMock: jest.SpyInstance; - let adminOrganisationService: AdminOrganisationService; - - beforeAll(() => { - consoleErrorMock = jest - .spyOn(console, "error") - .mockImplementation(() => {}); - }); - - afterAll(() => { - consoleErrorMock.mockRestore(); - }); - - beforeEach(() => { - adminOrganisationService = new AdminOrganisationService(); - }); - - describe("deleteOrganisation", () => { - it("should delete the organization successfully", async () => { - const orgId = "org123"; - const mockOrganization = { - id: orgId, - name: "Test Organization", - description: "This is a test organization", - createdAt: new Date(), - updatedAt: new Date(), - } as Organization; - - const mockRepository = { - findOne: jest.fn().mockResolvedValue(mockOrganization), - remove: jest.fn().mockResolvedValue(mockOrganization), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue( - mockRepository, - ); - - const result = await adminOrganisationService.deleteOrganization(orgId); - - expect(mockRepository.findOne).toHaveBeenCalledWith({ - where: { id: orgId }, - }); - expect(mockRepository.remove).toHaveBeenCalledWith(mockOrganization); - expect(result).toEqual(mockOrganization); // Ensure this matches the expected return value - }); - - it("should throw a 404 error if organization is not found", async () => { - const orgId = "nonexistentOrg"; - - const mockRepository = { - findOne: jest.fn().mockResolvedValue(null), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue( - mockRepository, - ); - - await expect( - adminOrganisationService.deleteOrganization(orgId), - ).rejects.toThrow(HttpError); - await expect( - adminOrganisationService.deleteOrganization(orgId), - ).rejects.toThrow("Organization not found"); - }); - - it("should throw an error if deletion fails", async () => { - const orgId = "org123"; - const mockOrganization = { - id: orgId, - name: "Test Organization", - description: "This is a test organization", - createdAt: new Date(), - updatedAt: new Date(), - } as Organization; - - const mockRepository = { - findOne: jest.fn().mockResolvedValue(mockOrganization), - remove: jest.fn().mockRejectedValue(new Error("Deletion failed")), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue( - mockRepository, - ); - - await expect( - adminOrganisationService.deleteOrganization(orgId), - ).rejects.toThrow(HttpError); - await expect( - adminOrganisationService.deleteOrganization(orgId), - ).rejects.toThrow("Deletion failed"); - }); - }); -}); +// @ts-nocheck +import AppDataSource from "../data-source"; +import { HttpError } from "../middleware"; +import { AdminOrganisationService } from "../services/admin.services"; + +jest.mock("../data-source"); +jest.mock("../utils/index"); + +describe("AdminUserService", () => { + let consoleErrorMock: jest.SpyInstance; + let adminOrganisationService: AdminOrganisationService; + + beforeAll(() => { + consoleErrorMock = jest + .spyOn(console, "error") + .mockImplementation(() => {}); + }); + + afterAll(() => { + consoleErrorMock.mockRestore(); + }); + + beforeEach(() => { + adminOrganisationService = new AdminOrganisationService(); + }); + + describe("deleteOrganisation", () => { + it("should delete the organization successfully", async () => { + const orgId = "org123"; + const mockOrganization = { + id: orgId, + name: "Test Organization", + description: "This is a test organization", + createdAt: new Date(), + updatedAt: new Date(), + } as Organization; + + const mockRepository = { + findOne: jest.fn().mockResolvedValue(mockOrganization), + remove: jest.fn().mockResolvedValue(mockOrganization), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue( + mockRepository, + ); + + const result = await adminOrganisationService.deleteOrganization(orgId); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { id: orgId }, + }); + expect(mockRepository.remove).toHaveBeenCalledWith(mockOrganization); + expect(result).toEqual(mockOrganization); // Ensure this matches the expected return value + }); + + it("should throw a 404 error if organization is not found", async () => { + const orgId = "nonexistentOrg"; + + const mockRepository = { + findOne: jest.fn().mockResolvedValue(null), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue( + mockRepository, + ); + + await expect( + adminOrganisationService.deleteOrganization(orgId), + ).rejects.toThrow(HttpError); + await expect( + adminOrganisationService.deleteOrganization(orgId), + ).rejects.toThrow("Organization not found"); + }); + + it("should throw an error if deletion fails", async () => { + const orgId = "org123"; + const mockOrganization = { + id: orgId, + name: "Test Organization", + description: "This is a test organization", + createdAt: new Date(), + updatedAt: new Date(), + } as Organization; + + const mockRepository = { + findOne: jest.fn().mockResolvedValue(mockOrganization), + remove: jest.fn().mockRejectedValue(new Error("Deletion failed")), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue( + mockRepository, + ); + + await expect( + adminOrganisationService.deleteOrganization(orgId), + ).rejects.toThrow(HttpError); + await expect( + adminOrganisationService.deleteOrganization(orgId), + ).rejects.toThrow("Deletion failed"); + }); + }); +}); diff --git a/src/test/superadminListUser.spec.ts b/src/test/superadminListUser.spec.ts index d9be6f03..9bb7c136 100644 --- a/src/test/superadminListUser.spec.ts +++ b/src/test/superadminListUser.spec.ts @@ -1,68 +1,68 @@ -// @ts-nocheck -import { AdminUserService } from "../services"; -import { User } from "../models"; -import AppDataSource from "../data-source"; -import { Repository } from "typeorm"; - -describe("AdminUserService", () => { - let adminUserService: AdminUserService; - let userRepository: Repository; - - beforeAll(() => { - adminUserService = new AdminUserService(); - userRepository = AppDataSource.getRepository(User); - }); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - afterAll(() => { - jest.setTimeout(3000); - }); - - it("should return paginated users", async () => { - const mockUsers = [ - { id: 1, name: "User1" }, - { id: 2, name: "User2" }, - { id: 3, name: "User3" }, - { id: 4, name: "User5" }, - { id: 5, name: "User6" }, - { id: 6, name: "User6" }, - ] as unknown as User[]; - - const findAndCount = jest - .spyOn(userRepository, "findAndCount") - .mockResolvedValue([mockUsers, 10]); - - const page = 1; - const limit = 2; - - const users = await adminUserService.getPaginatedUsers(page, limit); - - expect(findAndCount).toHaveBeenCalledWith({ - skip: (page - 1) * limit, - take: limit, - }); - - expect(users).toEqual({ users: mockUsers, totalUsers: 10 }); - }); - - it("should return empty array when no users are found", async () => { - const findAndCountSpy = jest - .spyOn(userRepository, "findAndCount") - .mockResolvedValue([[], 0]); - - const page = 1; - const limit = 2; - - const noUser = await adminUserService.getPaginatedUsers(page, limit); - - expect(findAndCountSpy).toHaveBeenCalledWith({ - skip: (page - 1) * limit, - take: limit, - }); - - expect(noUser).toEqual({ users: [], totalUsers: 0 }); - }); -}); +// @ts-nocheck +import { AdminUserService } from "../services"; +import { User } from "../models"; +import AppDataSource from "../data-source"; +import { Repository } from "typeorm"; + +describe("AdminUserService", () => { + let adminUserService: AdminUserService; + let userRepository: Repository; + + beforeAll(() => { + adminUserService = new AdminUserService(); + userRepository = AppDataSource.getRepository(User); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.setTimeout(3000); + }); + + it("should return paginated users", async () => { + const mockUsers = [ + { id: 1, name: "User1" }, + { id: 2, name: "User2" }, + { id: 3, name: "User3" }, + { id: 4, name: "User5" }, + { id: 5, name: "User6" }, + { id: 6, name: "User6" }, + ] as unknown as User[]; + + const findAndCount = jest + .spyOn(userRepository, "findAndCount") + .mockResolvedValue([mockUsers, 10]); + + const page = 1; + const limit = 2; + + const users = await adminUserService.getPaginatedUsers(page, limit); + + expect(findAndCount).toHaveBeenCalledWith({ + skip: (page - 1) * limit, + take: limit, + }); + + expect(users).toEqual({ users: mockUsers, totalUsers: 10 }); + }); + + it("should return empty array when no users are found", async () => { + const findAndCountSpy = jest + .spyOn(userRepository, "findAndCount") + .mockResolvedValue([[], 0]); + + const page = 1; + const limit = 2; + + const noUser = await adminUserService.getPaginatedUsers(page, limit); + + expect(findAndCountSpy).toHaveBeenCalledWith({ + skip: (page - 1) * limit, + take: limit, + }); + + expect(noUser).toEqual({ users: [], totalUsers: 0 }); + }); +}); diff --git a/src/test/superadminUpdateOrg.spec.ts b/src/test/superadminUpdateOrg.spec.ts index fca71431..f4c14d2c 100644 --- a/src/test/superadminUpdateOrg.spec.ts +++ b/src/test/superadminUpdateOrg.spec.ts @@ -1,122 +1,122 @@ -// @ts-nocheck -import { Request } from "express"; -import AppDataSource from "../data-source"; -import { Organization } from "../models"; -import { AdminOrganisationService } from "../services/admin.services"; -import { HttpError } from "../middleware"; - -jest.mock("../data-source"); -jest.mock("../models"); -jest.mock("../utils"); - -describe("Organisation", () => { - let adminOrganisationService: AdminOrganisationService; - let consoleErrorMock: jest.SpyInstance; - - beforeAll(() => { - consoleErrorMock = jest - .spyOn(console, "error") - .mockImplementation(() => {}); - }); - - afterAll(() => { - consoleErrorMock.mockRestore(); - }); - - beforeEach(() => { - adminOrganisationService = new AdminOrganisationService(); - }); - - describe("Update Organisation", () => { - it("should update the organosation successfully", async () => { - const req = { - body: { - name: "org2", - email: "org2@gmail.com", - country: "nigeria", - state: "nigeria", - }, - params: { id: "1" }, - } as unknown as Request; - - const mockOrg = { - id: "1", - name: "Org1", - email: "org1@gmail.com", - country: "nigeria", - state: "nigeria", - createdAt: new Date(), - // updatedAt: new Date(), - } as Organization; - - const updatedOrg = { - name: "org2", - email: "org2@gmail.com", - country: "nigeria", - state: "nigeria", - }; - - const mockUpdatedOrg = { - ...mockOrg, - ...updatedOrg, - address: undefined, - description: undefined, - industry: undefined, - type: undefined, - // updatedAt: new Date(), - }; - - const orgRepository = { - findOne: jest.fn().mockResolvedValue(mockOrg), - update: jest.fn().mockImplementation((id, fields) => { - Object.assign(mockOrg, fields); - return Promise.resolve(); - }), - findOneBy: jest.fn().mockResolvedValue(mockUpdatedOrg), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue(orgRepository); - const result = await adminOrganisationService.update(req); - - expect(orgRepository.findOne).toHaveBeenCalledWith({ - where: { id: "1" }, - }); - expect(orgRepository.update).toHaveBeenCalledWith("1", { - name: "org2", - email: "org2@gmail.com", - country: "nigeria", - state: "nigeria", - }); - expect(result).toEqual(mockUpdatedOrg); - }); - - it("should throw a 404 error if organisation is not found", async () => { - const req = { - body: { - // id: "2", - name: "Org3", - email: "org3@gmail.com", - country: "nigeria", - state: "nigeria", - }, - params: { id: "2" }, - } as unknown as Request; - - const orgRepository = { - findOne: jest.fn().mockResolvedValue(null), - }; - - (AppDataSource.getRepository as jest.Mock).mockReturnValue(orgRepository); - - await expect(adminOrganisationService.update(req)).rejects.toThrow( - HttpError, - ); - await expect(adminOrganisationService.update(req)).rejects.toThrow( - "Organisation not found, please check and try again", - ); - expect(orgRepository.findOne).toHaveBeenCalledWith({ - where: { id: "2" }, - }); - }); - }); -}); +// @ts-nocheck +import { Request } from "express"; +import AppDataSource from "../data-source"; +import { Organization } from "../models"; +import { AdminOrganisationService } from "../services/admin.services"; +import { HttpError } from "../middleware"; + +jest.mock("../data-source"); +jest.mock("../models"); +jest.mock("../utils"); + +describe("Organisation", () => { + let adminOrganisationService: AdminOrganisationService; + let consoleErrorMock: jest.SpyInstance; + + beforeAll(() => { + consoleErrorMock = jest + .spyOn(console, "error") + .mockImplementation(() => {}); + }); + + afterAll(() => { + consoleErrorMock.mockRestore(); + }); + + beforeEach(() => { + adminOrganisationService = new AdminOrganisationService(); + }); + + describe("Update Organisation", () => { + it("should update the organosation successfully", async () => { + const req = { + body: { + name: "org2", + email: "org2@gmail.com", + country: "nigeria", + state: "nigeria", + }, + params: { id: "1" }, + } as unknown as Request; + + const mockOrg = { + id: "1", + name: "Org1", + email: "org1@gmail.com", + country: "nigeria", + state: "nigeria", + createdAt: new Date(), + // updatedAt: new Date(), + } as Organization; + + const updatedOrg = { + name: "org2", + email: "org2@gmail.com", + country: "nigeria", + state: "nigeria", + }; + + const mockUpdatedOrg = { + ...mockOrg, + ...updatedOrg, + address: undefined, + description: undefined, + industry: undefined, + type: undefined, + // updatedAt: new Date(), + }; + + const orgRepository = { + findOne: jest.fn().mockResolvedValue(mockOrg), + update: jest.fn().mockImplementation((id, fields) => { + Object.assign(mockOrg, fields); + return Promise.resolve(); + }), + findOneBy: jest.fn().mockResolvedValue(mockUpdatedOrg), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue(orgRepository); + const result = await adminOrganisationService.update(req); + + expect(orgRepository.findOne).toHaveBeenCalledWith({ + where: { id: "1" }, + }); + expect(orgRepository.update).toHaveBeenCalledWith("1", { + name: "org2", + email: "org2@gmail.com", + country: "nigeria", + state: "nigeria", + }); + expect(result).toEqual(mockUpdatedOrg); + }); + + it("should throw a 404 error if organisation is not found", async () => { + const req = { + body: { + // id: "2", + name: "Org3", + email: "org3@gmail.com", + country: "nigeria", + state: "nigeria", + }, + params: { id: "2" }, + } as unknown as Request; + + const orgRepository = { + findOne: jest.fn().mockResolvedValue(null), + }; + + (AppDataSource.getRepository as jest.Mock).mockReturnValue(orgRepository); + + await expect(adminOrganisationService.update(req)).rejects.toThrow( + HttpError, + ); + await expect(adminOrganisationService.update(req)).rejects.toThrow( + "Organisation not found, please check and try again", + ); + expect(orgRepository.findOne).toHaveBeenCalledWith({ + where: { id: "2" }, + }); + }); + }); +}); diff --git a/src/types/express.d.ts b/src/types/express.d.ts index b7fd7b08..0f8d02fc 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -1,12 +1,12 @@ -import { UserRole } from "../enums/userRoles"; - -declare global { - namespace Express { - interface Request { - user?: { - userId: string; - role?: UserRole; - }; - } - } -} +import { UserRole } from "../enums/userRoles"; + +declare global { + namespace Express { + interface Request { + user?: { + userId: string; + role?: UserRole; + }; + } + } +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 81e8e02a..ee99bf36 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,125 +1,125 @@ -import { User } from "../models"; -import { Permissions } from "../models/permissions.entity"; - -export interface IUserService { - getUserById(id: string): Promise; - getAllUsers(): Promise; -} - -export interface IOrgService {} - -export interface IRole { - role: "super_admin" | "admin" | "user"; -} - -export interface IUserSignUp { - first_name: string; - last_name: string; - email: string; - password: string; -} -export interface IUserLogin { - email: string; - password: string; -} - -export interface IProduct { - name: string; - description: string; - price: number; - category: string; -} - -export interface IAuthService { - login(payload: IUserLogin): Promise; - signUp(payload: IUserSignUp, res: unknown): Promise; - verifyEmail(token: string, otp: number): Promise<{ message: string }>; - changePassword( - userId: string, - oldPassword: string, - newPassword: string, - confirmPassword: string, - ): Promise<{ message: string }>; - generateMagicLink(email: string): Promise<{ ok: boolean; message: string }>; - validateMagicLinkToken( - token: string, - ): Promise<{ status: string; email: string; userId: string }>; - passwordlessLogin(userId: string): Promise<{ access_token: string }>; -} - -export interface ICreateOrganisation { - name: string; - description: string; - email: string; - industry: string; - type: string; - country: string; - address: string; - state: string; -} - -export interface ICreateOrgRole { - name: string; - description: string; -} - -export interface IOrganisationService { - createOrganisation( - payload: ICreateOrganisation, - userId: string, - ): Promise; - removeUser(org_id: string, user_id: string): Promise; - - createOrganisationRole( - payload: ICreateOrgRole, - org_id: string, - ): Promise; -} - -declare module "express-serve-static-core" { - interface Request { - user?: User; - } -} - -export interface EmailQueuePayload { - templateId: string; - recipient: string; - variables?: Record; -} - -export interface GoogleUser { - email: string; - email_verified: boolean; - name: string; - picture: string; - sub: string; -} - -export interface INewsLetterSubscriptionService { - subscribeUser(email: string): Promise; - unSubcribeUser(email: string): Promise; -} - -export interface INewsLetterSubscription { - email: string; -} - -export type UserIdentifierOptionsType = - | { - identifierType: "id"; - identifier: string; - } - | { - identifierType: "email"; - identifier: string; - }; - -export type UpdateUserRecordOption = { - updatePayload: Partial; - identifierOption: UserIdentifierOptionsType; -}; - -export interface IBillingPlanService { - createBillingPlan(planData: Partial): Promise; -} +import { User } from "../models"; +import { Permissions } from "../models/permissions.entity"; + +export interface IUserService { + getUserById(id: string): Promise; + getAllUsers(): Promise; +} + +export interface IOrgService {} + +export interface IRole { + role: "super_admin" | "admin" | "user"; +} + +export interface IUserSignUp { + first_name: string; + last_name: string; + email: string; + password: string; +} +export interface IUserLogin { + email: string; + password: string; +} + +export interface IProduct { + name: string; + description: string; + price: number; + category: string; +} + +export interface IAuthService { + login(payload: IUserLogin): Promise; + signUp(payload: IUserSignUp, res: unknown): Promise; + verifyEmail(token: string, otp: number): Promise<{ message: string }>; + changePassword( + userId: string, + oldPassword: string, + newPassword: string, + confirmPassword: string, + ): Promise<{ message: string }>; + generateMagicLink(email: string): Promise<{ ok: boolean; message: string }>; + validateMagicLinkToken( + token: string, + ): Promise<{ status: string; email: string; userId: string }>; + passwordlessLogin(userId: string): Promise<{ access_token: string }>; +} + +export interface ICreateOrganisation { + name: string; + description: string; + email: string; + industry: string; + type: string; + country: string; + address: string; + state: string; +} + +export interface ICreateOrgRole { + name: string; + description: string; +} + +export interface IOrganisationService { + createOrganisation( + payload: ICreateOrganisation, + userId: string, + ): Promise; + removeUser(org_id: string, user_id: string): Promise; + + createOrganisationRole( + payload: ICreateOrgRole, + org_id: string, + ): Promise; +} + +declare module "express-serve-static-core" { + interface Request { + user?: User; + } +} + +export interface EmailQueuePayload { + templateId: string; + recipient: string; + variables?: Record; +} + +export interface GoogleUser { + email: string; + email_verified: boolean; + name: string; + picture: string; + sub: string; +} + +export interface INewsLetterSubscriptionService { + subscribeUser(email: string): Promise; + unSubcribeUser(email: string): Promise; +} + +export interface INewsLetterSubscription { + email: string; +} + +export type UserIdentifierOptionsType = + | { + identifierType: "id"; + identifier: string; + } + | { + identifierType: "email"; + identifier: string; + }; + +export type UpdateUserRecordOption = { + updatePayload: Partial; + identifierOption: UserIdentifierOptionsType; +}; + +export interface IBillingPlanService { + createBillingPlan(planData: Partial): Promise; +} diff --git a/src/utils/contactValidator.ts b/src/utils/contactValidator.ts index efbeb914..99c3d229 100644 --- a/src/utils/contactValidator.ts +++ b/src/utils/contactValidator.ts @@ -1,28 +1,28 @@ -interface ContactData { - name: string; - email: string; - message: string; -} - -export function validateContact(data: ContactData): string[] { - const errors: string[] = []; - - if (!data.name || data.name.length > 100) { - errors.push( - "Please enter your name. It should be less than 100 characters.", - ); - } - - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!data.email || !emailRegex.test(data.email)) { - errors.push("Please enter a valid email address."); - } - - if (!data.message || data.message.length > 250) { - errors.push( - "Please enter your message. It should be less than 250 characters.", - ); - } - - return errors; -} +interface ContactData { + name: string; + email: string; + message: string; +} + +export function validateContact(data: ContactData): string[] { + const errors: string[] = []; + + if (!data.name || data.name.length > 100) { + errors.push( + "Please enter your name. It should be less than 100 characters.", + ); + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!data.email || !emailRegex.test(data.email)) { + errors.push("Please enter a valid email address."); + } + + if (!data.message || data.message.length > 250) { + errors.push( + "Please enter your message. It should be less than 250 characters.", + ); + } + + return errors; +} diff --git a/src/utils/generate-reset-token.ts b/src/utils/generate-reset-token.ts index bf3dd8d4..a0f6bac9 100644 --- a/src/utils/generate-reset-token.ts +++ b/src/utils/generate-reset-token.ts @@ -1,18 +1,18 @@ -import crypto from "crypto"; - -const generateResetToken = (): { - resetToken: string; - hashedToken: string; - expiresAt: Date; -} => { - const resetToken = crypto.randomBytes(32).toString("hex"); - const hashedToken = crypto - .createHash("sha256") - .update(resetToken) - .digest("hex"); - const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes from now - - return { resetToken, hashedToken, expiresAt }; -}; - -export default generateResetToken; +import crypto from "crypto"; + +const generateResetToken = (): { + resetToken: string; + hashedToken: string; + expiresAt: Date; +} => { + const resetToken = crypto.randomBytes(32).toString("hex"); + const hashedToken = crypto + .createHash("sha256") + .update(resetToken) + .digest("hex"); + const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes from now + + return { resetToken, hashedToken, expiresAt }; +}; + +export default generateResetToken; diff --git a/src/utils/index.ts b/src/utils/index.ts index 05d25f6e..01b7c483 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,53 +1,53 @@ -import * as bcrypt from "bcryptjs"; -import rateLimit from "express-rate-limit"; -import jwt from "jsonwebtoken"; -import config from "../config"; - -export const getIsInvalidMessage = (fieldLabel: string) => - `${fieldLabel} is invalid`; - -export async function hashPassword(password: string): Promise { - return await bcrypt.hash(password, 10); -} - -export async function comparePassword( - password: string, - hashedPassword: string, -): Promise { - return await bcrypt.compare(password, hashedPassword); -} - -export async function generateAccessToken(user_id: string) { - return jwt.sign({ user_id }, config.TOKEN_SECRET, { expiresIn: "1d" }); -} - -export const generateNumericOTP = (length: number): string => { - let otp = ""; - for (let i = 0; i < length; i++) { - otp += Math.floor(Math.random() * 9 + 1).toString(); - } - return otp; -}; - -export const generateToken = (payload: Record) => { - return jwt.sign(payload, config.TOKEN_SECRET, { - expiresIn: config.TOKEN_EXPIRY, - }); -}; - -export const verifyToken = (token: string): Record | null => { - try { - const payload = jwt.verify(token, config.TOKEN_SECRET); - return payload as Record; - } catch (error) { - return { - error: error.message, - }; - } -}; - -export const Limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // Limit each IP to 100 requests per windowMs - message: "Too many requests from this IP, please try again after 15 minutes", -}); +import * as bcrypt from "bcryptjs"; +import rateLimit from "express-rate-limit"; +import jwt from "jsonwebtoken"; +import config from "../config"; + +export const getIsInvalidMessage = (fieldLabel: string) => + `${fieldLabel} is invalid`; + +export async function hashPassword(password: string): Promise { + return await bcrypt.hash(password, 10); +} + +export async function comparePassword( + password: string, + hashedPassword: string, +): Promise { + return await bcrypt.compare(password, hashedPassword); +} + +export async function generateAccessToken(user_id: string) { + return jwt.sign({ user_id }, config.TOKEN_SECRET, { expiresIn: "1d" }); +} + +export const generateNumericOTP = (length: number): string => { + let otp = ""; + for (let i = 0; i < length; i++) { + otp += Math.floor(Math.random() * 9 + 1).toString(); + } + return otp; +}; + +export const generateToken = (payload: Record) => { + return jwt.sign(payload, config.TOKEN_SECRET, { + expiresIn: config.TOKEN_EXPIRY, + }); +}; + +export const verifyToken = (token: string): Record | null => { + try { + const payload = jwt.verify(token, config.TOKEN_SECRET); + return payload as Record; + } catch (error) { + return { + error: error.message, + }; + } +}; + +export const Limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // Limit each IP to 100 requests per windowMs + message: "Too many requests from this IP, please try again after 15 minutes", +}); diff --git a/src/utils/isSuperAdmin.ts b/src/utils/isSuperAdmin.ts index bcb8d311..7c372b4c 100644 --- a/src/utils/isSuperAdmin.ts +++ b/src/utils/isSuperAdmin.ts @@ -1,11 +1,11 @@ -import { User } from "../models"; -import AppDataSource from "../data-source"; -import { UserRole } from "../enums/userRoles"; - -const userRepository = AppDataSource.getRepository(User); -const isSuperAdmin = async (userId: string): Promise => { - const user = await userRepository.findOneBy({ id: userId }); - return user?.role === UserRole.SUPER_ADMIN; -}; - -export default isSuperAdmin; +import { User } from "../models"; +import AppDataSource from "../data-source"; +import { UserRole } from "../enums/userRoles"; + +const userRepository = AppDataSource.getRepository(User); +const isSuperAdmin = async (userId: string): Promise => { + const user = await userRepository.findOneBy({ id: userId }); + return user?.role === UserRole.SUPER_ADMIN; +}; + +export default isSuperAdmin; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index e8f54ae5..330623a0 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,14 +1,14 @@ -import pino from "pino"; -import dayjs from "dayjs"; - -const log = pino({ - transport: { - target: "pino-pretty", - }, - base: { - pid: false, - }, - timestamp: () => `,"time":"${dayjs().format()}"`, -}); - -export default log; +import pino from "pino"; +import dayjs from "dayjs"; + +const log = pino({ + transport: { + target: "pino-pretty", + }, + base: { + pid: false, + }, + timestamp: () => `,"time":"${dayjs().format()}"`, +}); + +export default log; diff --git a/src/utils/mail.ts b/src/utils/mail.ts index c67bb0f2..64064c31 100644 --- a/src/utils/mail.ts +++ b/src/utils/mail.ts @@ -1,26 +1,26 @@ -import nodemailer from "nodemailer"; -import config from "../config"; -import { BadRequest } from "../middleware"; -import log from "./logger"; - -const Sendmail = async (emailcontent: any) => { - const transporter = nodemailer.createTransport({ - service: config.SMTP_SERVICE, - host: "smtp.gmail.com", - port: 587, - secure: false, - auth: { - user: config.SMTP_USER, - pass: config.SMTP_PASSWORD, - }, - }); - try { - await transporter.sendMail(emailcontent); - return "Email sent successfully."; - } catch (error) { - log.error(error); - throw new BadRequest("Error sending email"); - } -}; - -export { Sendmail }; +import nodemailer from "nodemailer"; +import config from "../config"; +import { BadRequest } from "../middleware"; +import log from "./logger"; + +const Sendmail = async (emailcontent: any) => { + const transporter = nodemailer.createTransport({ + service: config.SMTP_SERVICE, + host: "smtp.gmail.com", + port: 587, + secure: false, + auth: { + user: config.SMTP_USER, + pass: config.SMTP_PASSWORD, + }, + }); + try { + await transporter.sendMail(emailcontent); + return "Email sent successfully."; + } catch (error) { + log.error(error); + throw new BadRequest("Error sending email"); + } +}; + +export { Sendmail }; diff --git a/src/utils/queue.ts b/src/utils/queue.ts index 5fb9d727..2c9de870 100644 --- a/src/utils/queue.ts +++ b/src/utils/queue.ts @@ -1,150 +1,150 @@ -import Bull, { Job } from "bull"; -import config from "../config"; -import smsServices from "../services/sms.services"; -import logs from "./logger"; -import { Sendmail } from "./mail"; - -interface EmailData { - from: string; - to: string; - subject: string; - html: string; -} - -interface SmsData { - sender_id: string; - message: string; - phone_number: string; -} - -const retries: number = 3; -const delay = 1000 * 60 * 5; - -const redisConfig = { - host: config.REDIS_HOST, - port: Number(config.REDIS_PORT), - // password: config.REDIS_PASSWORD, -}; - -const emailQueue = new Bull("Email", { - redis: redisConfig, -}); - -const addEmailToQueue = async (data: EmailData) => { - await emailQueue.add(data, { - attempts: retries, - backoff: { - type: "fixed", - delay, - }, - }); -}; - -emailQueue.process(async (job: Job, done) => { - try { - await Sendmail(job.data); - job.log("Email sent successfully to " + job.data.to); - logs.info("Email sent successfully"); - } catch (error) { - logs.error("Error sending email:", error); - throw error; - } finally { - done(); - } -}); - -// Notification Queue -const notificationQueue = new Bull("Notification", { - redis: redisConfig, -}); - -const addNotificationToQueue = async (data: any) => { - await notificationQueue.add(data, { - attempts: retries, - backoff: { - type: "fixed", - delay, - }, - }); -}; - -notificationQueue.process(async (job: Job, done) => { - try { - // sending Notification Function - job.log("Notification sent successfully to " + job.data.to); - logs.info("Notification sent successfully"); - } catch (error) { - logs.error("Error sending notification:", error); - throw error; - } finally { - done(); - } -}); - -// SMS Queue -const smsQueue = new Bull("SMS", { - redis: redisConfig, -}); - -const addSmsToQueue = async (data: SmsData) => { - await smsQueue.add(data, { - attempts: retries, - backoff: { - type: "fixed", - delay, - }, - }); -}; - -smsQueue.process(async (job: Job, done) => { - try { - const { sender_id, message, phone_number } = job.data; - await smsServices.sendSms(sender_id, phone_number, message); - job.log("SMS sent successfully to " + job.data); - logs.info("SMS sent successfully"); - } catch (error) { - logs.error("Error sending SMS:", error); - throw error; - } finally { - done(); - } -}); - -smsQueue.on("completed", (job: Job) => { - logs.info(`Job with id ${job.id} has been completed`); -}); - -smsQueue.on("failed", (job: Job, error: Error) => { - logs.error( - `Job with id ${job.id} has been failed with error: ${error.message}`, - ); -}); - -notificationQueue.on("completed", (job: Job) => { - logs.info(`Job with id ${job.id} has been completed`); -}); - -notificationQueue.on("failed", (job: Job, error: Error) => { - logs.error( - `Job with id ${job.id} has been failed with error: ${error.message}`, - ); -}); - -emailQueue.on("completed", (job: Job) => { - logs.info(`Job with id ${job.id} has been completed`); -}); - -emailQueue.on("failed", (job: Job, error: Error) => { - logs.error( - `Job with id ${job.id} has been failed with error: ${error.message}`, - ); -}); - -export { - addEmailToQueue, - addNotificationToQueue, - addSmsToQueue, - emailQueue, - notificationQueue, - smsQueue, -}; +import Bull, { Job } from "bull"; +import config from "../config"; +import smsServices from "../services/sms.services"; +import logs from "./logger"; +import { Sendmail } from "./mail"; + +interface EmailData { + from: string; + to: string; + subject: string; + html: string; +} + +interface SmsData { + sender_id: string; + message: string; + phone_number: string; +} + +const retries: number = 3; +const delay = 1000 * 60 * 5; + +const redisConfig = { + host: config.REDIS_HOST, + port: Number(config.REDIS_PORT), + // password: config.REDIS_PASSWORD, +}; + +const emailQueue = new Bull("Email", { + redis: redisConfig, +}); + +const addEmailToQueue = async (data: EmailData) => { + await emailQueue.add(data, { + attempts: retries, + backoff: { + type: "fixed", + delay, + }, + }); +}; + +emailQueue.process(async (job: Job, done) => { + try { + await Sendmail(job.data); + job.log("Email sent successfully to " + job.data.to); + logs.info("Email sent successfully"); + } catch (error) { + logs.error("Error sending email:", error); + throw error; + } finally { + done(); + } +}); + +// Notification Queue +const notificationQueue = new Bull("Notification", { + redis: redisConfig, +}); + +const addNotificationToQueue = async (data: any) => { + await notificationQueue.add(data, { + attempts: retries, + backoff: { + type: "fixed", + delay, + }, + }); +}; + +notificationQueue.process(async (job: Job, done) => { + try { + // sending Notification Function + job.log("Notification sent successfully to " + job.data.to); + logs.info("Notification sent successfully"); + } catch (error) { + logs.error("Error sending notification:", error); + throw error; + } finally { + done(); + } +}); + +// SMS Queue +const smsQueue = new Bull("SMS", { + redis: redisConfig, +}); + +const addSmsToQueue = async (data: SmsData) => { + await smsQueue.add(data, { + attempts: retries, + backoff: { + type: "fixed", + delay, + }, + }); +}; + +smsQueue.process(async (job: Job, done) => { + try { + const { sender_id, message, phone_number } = job.data; + await smsServices.sendSms(sender_id, phone_number, message); + job.log("SMS sent successfully to " + job.data); + logs.info("SMS sent successfully"); + } catch (error) { + logs.error("Error sending SMS:", error); + throw error; + } finally { + done(); + } +}); + +smsQueue.on("completed", (job: Job) => { + logs.info(`Job with id ${job.id} has been completed`); +}); + +smsQueue.on("failed", (job: Job, error: Error) => { + logs.error( + `Job with id ${job.id} has been failed with error: ${error.message}`, + ); +}); + +notificationQueue.on("completed", (job: Job) => { + logs.info(`Job with id ${job.id} has been completed`); +}); + +notificationQueue.on("failed", (job: Job, error: Error) => { + logs.error( + `Job with id ${job.id} has been failed with error: ${error.message}`, + ); +}); + +emailQueue.on("completed", (job: Job) => { + logs.info(`Job with id ${job.id} has been completed`); +}); + +emailQueue.on("failed", (job: Job, error: Error) => { + logs.error( + `Job with id ${job.id} has been failed with error: ${error.message}`, + ); +}); + +export { + addEmailToQueue, + addNotificationToQueue, + addSmsToQueue, + emailQueue, + notificationQueue, + smsQueue, +}; diff --git a/src/utils/request.utils.ts b/src/utils/request.utils.ts index 3c282c7f..864392f3 100644 --- a/src/utils/request.utils.ts +++ b/src/utils/request.utils.ts @@ -1,26 +1,26 @@ -import { Request, Response } from "express"; -import { User } from "../models"; - -class RequestUtils { - private request: Request; - private response: Response; - - constructor(req: Request, res: Response) { - this.request = req; - this.response = res; - } - - public addDataToState(key: string, data: any) { - return (this.response.locals[key] = data); - } - - public getDataFromState(key: string) { - return this.response.locals[key] || null; - } - - public getRequestUser() { - return this.response.locals.user as User; - } -} - -export default RequestUtils; +import { Request, Response } from "express"; +import { User } from "../models"; + +class RequestUtils { + private request: Request; + private response: Response; + + constructor(req: Request, res: Response) { + this.request = req; + this.response = res; + } + + public addDataToState(key: string, data: any) { + return (this.response.locals[key] = data); + } + + public getDataFromState(key: string) { + return this.response.locals[key] || null; + } + + public getRequestUser() { + return this.response.locals.user as User; + } +} + +export default RequestUtils; diff --git a/src/views/bull-board.ts b/src/views/bull-board.ts index 1b145a17..a95df2d3 100644 --- a/src/views/bull-board.ts +++ b/src/views/bull-board.ts @@ -1,18 +1,18 @@ -import { createBullBoard } from "@bull-board/api"; -import { ExpressAdapter } from "@bull-board/express"; -import { emailQueue, notificationQueue, smsQueue } from "../utils/queue"; -import { BullAdapter } from "@bull-board/api/bullAdapter"; - -const ServerAdapter = new ExpressAdapter(); - -createBullBoard({ - queues: [ - new BullAdapter(emailQueue), - new BullAdapter(notificationQueue), - new BullAdapter(smsQueue), - ], - serverAdapter: ServerAdapter, -}); - -ServerAdapter.setBasePath("/api/v1/queues"); -export default ServerAdapter; +import { createBullBoard } from "@bull-board/api"; +import { ExpressAdapter } from "@bull-board/express"; +import { emailQueue, notificationQueue, smsQueue } from "../utils/queue"; +import { BullAdapter } from "@bull-board/api/bullAdapter"; + +const ServerAdapter = new ExpressAdapter(); + +createBullBoard({ + queues: [ + new BullAdapter(emailQueue), + new BullAdapter(notificationQueue), + new BullAdapter(smsQueue), + ], + serverAdapter: ServerAdapter, +}); + +ServerAdapter.setBasePath("/api/v1/queues"); +export default ServerAdapter; diff --git a/src/views/email/renderTemplate.ts b/src/views/email/renderTemplate.ts index 9537b3db..b37af5d6 100644 --- a/src/views/email/renderTemplate.ts +++ b/src/views/email/renderTemplate.ts @@ -1,46 +1,46 @@ -import path from "path"; -import fs from "fs" -import Handlebars from "handlebars"; - - -const baseTemplateSource = fs.readFileSync(path.join(__dirname, 'templates', 'base_template.hbs'), 'utf8'); -Handlebars.registerPartial('base_template', baseTemplateSource); - - -function renderTemplate(templateName:string, variables:{}) { - const data = { - logoUrl: "https://example.com/logo.png", - imageUrl: "https://example.com/reset-password.png", - companyName: "Boilerplate", - supportUrl: "https://example.com/support", - socialIcons: [ - { - url: "https://facebook.com", - imgSrc: - "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png", - alt: "Facebook", - }, - { - url: "https://twitter.com", - imgSrc: - "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png", - alt: "Twitter", - }, - { - url: "https://instagram.com", - imgSrc: - "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png", - alt: "Instagram", - }, - ], - companyWebsite: "https://example.com", - preferencesUrl: "https://example.com/preferences", - unsubscribeUrl: "https://example.com/unsubscribe", - }; - const newData = {...data, ...variables} - const templateSource = fs.readFileSync(path.join(__dirname, 'templates', `${templateName}.hbs`), 'utf8'); - const template = Handlebars.compile(templateSource); - return template(newData); -} - -export default renderTemplate +import path from "path"; +import fs from "fs" +import Handlebars from "handlebars"; + + +const baseTemplateSource = fs.readFileSync(path.join(__dirname, 'templates', 'base_template.hbs'), 'utf8'); +Handlebars.registerPartial('base_template', baseTemplateSource); + + +function renderTemplate(templateName:string, variables:{}) { + const data = { + logoUrl: "https://example.com/logo.png", + imageUrl: "https://example.com/reset-password.png", + companyName: "Boilerplate", + supportUrl: "https://example.com/support", + socialIcons: [ + { + url: "https://facebook.com", + imgSrc: + "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png", + alt: "Facebook", + }, + { + url: "https://twitter.com", + imgSrc: + "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png", + alt: "Twitter", + }, + { + url: "https://instagram.com", + imgSrc: + "https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png", + alt: "Instagram", + }, + ], + companyWebsite: "https://example.com", + preferencesUrl: "https://example.com/preferences", + unsubscribeUrl: "https://example.com/unsubscribe", + }; + const newData = {...data, ...variables} + const templateSource = fs.readFileSync(path.join(__dirname, 'templates', `${templateName}.hbs`), 'utf8'); + const template = Handlebars.compile(templateSource); + return template(newData); +} + +export default renderTemplate diff --git a/src/views/email/templates/account-activation-request.hbs b/src/views/email/templates/account-activation-request.hbs index d81e193b..3fe4ad6f 100644 --- a/src/views/email/templates/account-activation-request.hbs +++ b/src/views/email/templates/account-activation-request.hbs @@ -1,12 +1,12 @@ - -{{#> base_template}} - {{#*inline "content"}} -

{{title}}

-

Hi {{userName}},

-

We recently detected a login attempt to your account from an unfamiliar device. To ensure the security of your account, we haven't granted access.

-

To activate your account and secure it, please click the button below:

- - {{/inline}} + +{{#> base_template}} + {{#*inline "content"}} +

{{title}}

+

Hi {{userName}},

+

We recently detected a login attempt to your account from an unfamiliar device. To ensure the security of your account, we haven't granted access.

+

To activate your account and secure it, please click the button below:

+ + {{/inline}} {{/base_template}} \ No newline at end of file diff --git a/src/views/email/templates/account-activation-successful.hbs b/src/views/email/templates/account-activation-successful.hbs index e374748d..6f2a3fe1 100644 --- a/src/views/email/templates/account-activation-successful.hbs +++ b/src/views/email/templates/account-activation-successful.hbs @@ -1,14 +1,14 @@ - -{{#> base_template}} - {{#*inline "content"}} - -

{{title}}

-

Hi {{userName}},

-

Congratulations! Your account with Boilerplate is now active and ready to use.

-

We're thrilled to have you as part of our community and look forward to helping you make the most out of your experience with us.

-

You can now log in and start exploring all the features and benefits we have to offer.

-

Thank you for joining Boilerplate!

- {{/inline}} -{{/base_template}} - + +{{#> base_template}} + {{#*inline "content"}} + +

{{title}}

+

Hi {{userName}},

+

Congratulations! Your account with Boilerplate is now active and ready to use.

+

We're thrilled to have you as part of our community and look forward to helping you make the most out of your experience with us.

+

You can now log in and start exploring all the features and benefits we have to offer.

+

Thank you for joining Boilerplate!

+ {{/inline}} +{{/base_template}} + \ No newline at end of file diff --git a/src/views/email/templates/base_template.hbs b/src/views/email/templates/base_template.hbs index fd9f47d3..67a7982a 100644 --- a/src/views/email/templates/base_template.hbs +++ b/src/views/email/templates/base_template.hbs @@ -1,123 +1,123 @@ - - - - - - - {{title}} - - - -
-
- Boilerplate -
-
- {{> content}} -
- -
- - + + + + + + + {{title}} + + + +
+
+ Boilerplate +
+
+ {{> content}} +
+ +
+ + diff --git a/src/views/email/templates/custom-email.hbs b/src/views/email/templates/custom-email.hbs index cb9820eb..7a3f7b98 100644 --- a/src/views/email/templates/custom-email.hbs +++ b/src/views/email/templates/custom-email.hbs @@ -1,11 +1,11 @@ - -{{#> base_template}} - {{#*inline "content"}} - -

{{title}}

-

Hi {{userName}},

-
- {{{body}}} -
- {{/inline}} -{{/base_template}} + +{{#> base_template}} + {{#*inline "content"}} + +

{{title}}

+

Hi {{userName}},

+
+ {{{body}}} +
+ {{/inline}} +{{/base_template}} diff --git a/src/views/email/templates/expired-account-activation-link.hbs b/src/views/email/templates/expired-account-activation-link.hbs index 66bcb459..65ee32e7 100644 --- a/src/views/email/templates/expired-account-activation-link.hbs +++ b/src/views/email/templates/expired-account-activation-link.hbs @@ -1,13 +1,13 @@ - - {{#> base_template}} - {{#*inline "content"}} -

{{title}}

-

Hi {{userName}},

-

We noticed that your account activation link has expired. For your security, activation links are only valid for a specific time period.

-

Don't worry, you can easily request a new activation link by clicking the button below:

- - {{/inline}} -{{/base_template}} + + {{#> base_template}} + {{#*inline "content"}} +

{{title}}

+

Hi {{userName}},

+

We noticed that your account activation link has expired. For your security, activation links are only valid for a specific time period.

+

Don't worry, you can easily request a new activation link by clicking the button below:

+ + {{/inline}} +{{/base_template}} \ No newline at end of file diff --git a/src/views/email/templates/new-activation-link-sent.hbs b/src/views/email/templates/new-activation-link-sent.hbs index 95671cb7..dd1c635f 100644 --- a/src/views/email/templates/new-activation-link-sent.hbs +++ b/src/views/email/templates/new-activation-link-sent.hbs @@ -1,13 +1,13 @@ - -{{#> base_template}} - {{#*inline "content"}} - -

{{title}}

-

Hi {{userName}},

-

We have sent you a new activation link for your Boilerplate account. Please click the button below to activate your account:

- - -{{/inline}} -{{/base_template}} + +{{#> base_template}} + {{#*inline "content"}} + +

{{title}}

+

Hi {{userName}},

+

We have sent you a new activation link for your Boilerplate account. Please click the button below to activate your account:

+ + +{{/inline}} +{{/base_template}} diff --git a/src/views/email/templates/password-reset-complete.hbs b/src/views/email/templates/password-reset-complete.hbs index 70369b14..fec154db 100644 --- a/src/views/email/templates/password-reset-complete.hbs +++ b/src/views/email/templates/password-reset-complete.hbs @@ -1,18 +1,18 @@ - -{{#> base_template}} - {{#*inline "content"}} - -

{{title}}

-

Hi {{userName}},

- -

The password for your Boilerplate account has been successfully changed. You can now continue to access your account as usual.

- -

If this wasn't done by you, please immediately reset the password to your Boilerplate account by following the steps below:

- -
    -
  1. Recover your account here: {{accountRecoverUrl}}
  2. -
  3. Review your phone numbers and email addresses and remove the ones that don’t belong to you once you gain access to your account.
  4. -
-{{/inline}} -{{/base_template}} + +{{#> base_template}} + {{#*inline "content"}} + +

{{title}}

+

Hi {{userName}},

+ +

The password for your Boilerplate account has been successfully changed. You can now continue to access your account as usual.

+ +

If this wasn't done by you, please immediately reset the password to your Boilerplate account by following the steps below:

+ +
    +
  1. Recover your account here: {{accountRecoverUrl}}
  2. +
  3. Review your phone numbers and email addresses and remove the ones that don’t belong to you once you gain access to your account.
  4. +
+{{/inline}} +{{/base_template}} \ No newline at end of file diff --git a/src/views/email/templates/password-reset.hbs b/src/views/email/templates/password-reset.hbs index 5426b2a6..98588f88 100644 --- a/src/views/email/templates/password-reset.hbs +++ b/src/views/email/templates/password-reset.hbs @@ -1,14 +1,14 @@ - -{{#> base_template}} - {{#*inline "content"}} - -

{{title}}

-

Hi {{userName}},

-

You recently requested to reset your password. If you did not make this request, you can ignore this email.

-

To reset your password, please click the button below.

- -{{/inline}} -{{/base_template}} + +{{#> base_template}} + {{#*inline "content"}} + +

{{title}}

+

Hi {{userName}},

+

You recently requested to reset your password. If you did not make this request, you can ignore this email.

+

To reset your password, please click the button below.

+ +{{/inline}} +{{/base_template}} \ No newline at end of file diff --git a/src/views/email/tests/account-activation-request.spec.ts b/src/views/email/tests/account-activation-request.spec.ts index 89ab7f53..006b2647 100644 --- a/src/views/email/tests/account-activation-request.spec.ts +++ b/src/views/email/tests/account-activation-request.spec.ts @@ -1,52 +1,52 @@ -import { describe, expect, it, test } from '@jest/globals'; -import fs from 'fs-extra'; -import Handlebars from 'handlebars'; -import path from 'path'; - -// Load the template -const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); -Handlebars.registerPartial('base_template', baseTemplateSource); -const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/account-activation-request.hbs'), 'utf8'); -const template = Handlebars.compile(templateSource); - -// Sample data to pass to the template -const data = { - title: 'Activate Your Account', - logoUrl: 'https://example.com/logo.png', - // imageUrl: 'https://example.com/reset-password.png', - userName: 'John Doe', - activationLinkUrl: 'https://example.com/activate-account', - companyName: 'Boilerplate', - supportUrl: 'https://example.com/support', - socialIcons: [ - { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, - { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, - { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } - ], - companyWebsite: 'https://example.com', - preferencesUrl: 'https://example.com/preferences', - unsubscribeUrl: 'https://example.com/unsubscribe' -}; - -describe('Email Template', () => { - it('should render correctly with provided data', () => { - const result = template(data); - - // Check for the presence of critical elements - expect(result).toContain(data.title); - expect(result).toContain(data.logoUrl); - // expect(result).toContain(data.imageUrl); - expect(result).toContain(`Hi ${data.userName}`); - expect(result).toContain(data.activationLinkUrl); - expect(result).toContain(data.companyName); - expect(result).toContain(data.supportUrl); - data.socialIcons.forEach(icon => { - expect(result).toContain(icon.url); - expect(result).toContain(icon.imgSrc); - expect(result).toContain(icon.alt); - }); - expect(result).toContain(data.companyWebsite); - expect(result).toContain(data.preferencesUrl); - expect(result).toContain(data.unsubscribeUrl); - }); -}); +import { describe, expect, it, test } from '@jest/globals'; +import fs from 'fs-extra'; +import Handlebars from 'handlebars'; +import path from 'path'; + +// Load the template +const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); +Handlebars.registerPartial('base_template', baseTemplateSource); +const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/account-activation-request.hbs'), 'utf8'); +const template = Handlebars.compile(templateSource); + +// Sample data to pass to the template +const data = { + title: 'Activate Your Account', + logoUrl: 'https://example.com/logo.png', + // imageUrl: 'https://example.com/reset-password.png', + userName: 'John Doe', + activationLinkUrl: 'https://example.com/activate-account', + companyName: 'Boilerplate', + supportUrl: 'https://example.com/support', + socialIcons: [ + { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, + { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, + { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } + ], + companyWebsite: 'https://example.com', + preferencesUrl: 'https://example.com/preferences', + unsubscribeUrl: 'https://example.com/unsubscribe' +}; + +describe('Email Template', () => { + it('should render correctly with provided data', () => { + const result = template(data); + + // Check for the presence of critical elements + expect(result).toContain(data.title); + expect(result).toContain(data.logoUrl); + // expect(result).toContain(data.imageUrl); + expect(result).toContain(`Hi ${data.userName}`); + expect(result).toContain(data.activationLinkUrl); + expect(result).toContain(data.companyName); + expect(result).toContain(data.supportUrl); + data.socialIcons.forEach(icon => { + expect(result).toContain(icon.url); + expect(result).toContain(icon.imgSrc); + expect(result).toContain(icon.alt); + }); + expect(result).toContain(data.companyWebsite); + expect(result).toContain(data.preferencesUrl); + expect(result).toContain(data.unsubscribeUrl); + }); +}); diff --git a/src/views/email/tests/account-activation-successful.spec.ts b/src/views/email/tests/account-activation-successful.spec.ts index 46b53820..87e8f2e2 100644 --- a/src/views/email/tests/account-activation-successful.spec.ts +++ b/src/views/email/tests/account-activation-successful.spec.ts @@ -1,51 +1,51 @@ -import { describe, expect, it, test } from '@jest/globals'; -import fs from 'fs-extra'; -import Handlebars from 'handlebars'; -import path from 'path'; - -const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); -Handlebars.registerPartial('base_template', baseTemplateSource); - -// Load the template -const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/account-activation-successful.hbs'), 'utf8'); -const template = Handlebars.compile(templateSource); - -// Sample data to pass to the template -const data = { - title: 'Your Account is Now Active!', - logoUrl: 'https://example.com/logo.png', - // imageUrl: 'https://example.com/example.png', - userName: 'John Doe', - companyName: 'Boilerplate', - supportUrl: 'https://example.com/support', - socialIcons: [ - { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, - { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, - { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } - ], - companyWebsite: 'https://example.com', - preferencesUrl: 'https://example.com/preferences', - unsubscribeUrl: 'https://example.com/unsubscribe' -}; - -describe('Email Template', () => { - it('should render correctly with provided data', () => { - const result = template(data); - - // Check for the presence of critical elements - expect(result).toContain(data.title); - expect(result).toContain(data.logoUrl); - // expect(result).toContain(data.imageUrl); - expect(result).toContain(`Hi ${data.userName}`); - expect(result).toContain(data.companyName); - expect(result).toContain(data.supportUrl); - data.socialIcons.forEach(icon => { - expect(result).toContain(icon.url); - expect(result).toContain(icon.imgSrc); - expect(result).toContain(icon.alt); - }); - expect(result).toContain(data.companyWebsite); - expect(result).toContain(data.preferencesUrl); - expect(result).toContain(data.unsubscribeUrl); - }); -}); +import { describe, expect, it, test } from '@jest/globals'; +import fs from 'fs-extra'; +import Handlebars from 'handlebars'; +import path from 'path'; + +const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); +Handlebars.registerPartial('base_template', baseTemplateSource); + +// Load the template +const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/account-activation-successful.hbs'), 'utf8'); +const template = Handlebars.compile(templateSource); + +// Sample data to pass to the template +const data = { + title: 'Your Account is Now Active!', + logoUrl: 'https://example.com/logo.png', + // imageUrl: 'https://example.com/example.png', + userName: 'John Doe', + companyName: 'Boilerplate', + supportUrl: 'https://example.com/support', + socialIcons: [ + { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, + { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, + { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } + ], + companyWebsite: 'https://example.com', + preferencesUrl: 'https://example.com/preferences', + unsubscribeUrl: 'https://example.com/unsubscribe' +}; + +describe('Email Template', () => { + it('should render correctly with provided data', () => { + const result = template(data); + + // Check for the presence of critical elements + expect(result).toContain(data.title); + expect(result).toContain(data.logoUrl); + // expect(result).toContain(data.imageUrl); + expect(result).toContain(`Hi ${data.userName}`); + expect(result).toContain(data.companyName); + expect(result).toContain(data.supportUrl); + data.socialIcons.forEach(icon => { + expect(result).toContain(icon.url); + expect(result).toContain(icon.imgSrc); + expect(result).toContain(icon.alt); + }); + expect(result).toContain(data.companyWebsite); + expect(result).toContain(data.preferencesUrl); + expect(result).toContain(data.unsubscribeUrl); + }); +}); diff --git a/src/views/email/tests/expired-account-activation-link.spec.ts b/src/views/email/tests/expired-account-activation-link.spec.ts index 0e1a355b..25f9c7c3 100644 --- a/src/views/email/tests/expired-account-activation-link.spec.ts +++ b/src/views/email/tests/expired-account-activation-link.spec.ts @@ -1,52 +1,52 @@ -import { describe, expect, it, test } from '@jest/globals'; -import fs from 'fs-extra'; -import Handlebars from 'handlebars'; -import path from 'path'; - -// Load the template -const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/expired-account-activation-link.hbs'), 'utf8'); -const template = Handlebars.compile(templateSource); -const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); -Handlebars.registerPartial('base_template', baseTemplateSource); - -// Sample data to pass to the template -const data = { - title: 'Activation Link Expired', - logoUrl: 'https://example.com/logo.png', - // imageUrl: 'https://example.com/expired-linkt.png', - userName: 'John Doe', - activationLinkUrl: 'https://example.com/activate-account', - companyName: 'Boilerplate', - supportUrl: 'https://example.com/support', - socialIcons: [ - { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, - { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, - { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } - ], - companyWebsite: 'https://example.com', - preferencesUrl: 'https://example.com/preferences', - unsubscribeUrl: 'https://example.com/unsubscribe' -}; - -describe('Email Template', () => { - it('should render correctly with provided data', () => { - const result = template(data); - - // Check for the presence of critical elements - expect(result).toContain(data.title); - expect(result).toContain(data.logoUrl); - // expect(result).toContain(data.imageUrl); - expect(result).toContain(`Hi ${data.userName}`); - expect(result).toContain(data.activationLinkUrl); - expect(result).toContain(data.companyName); - expect(result).toContain(data.supportUrl); - data.socialIcons.forEach(icon => { - expect(result).toContain(icon.url); - expect(result).toContain(icon.imgSrc); - expect(result).toContain(icon.alt); - }); - expect(result).toContain(data.companyWebsite); - expect(result).toContain(data.preferencesUrl); - expect(result).toContain(data.unsubscribeUrl); - }); -}); +import { describe, expect, it, test } from '@jest/globals'; +import fs from 'fs-extra'; +import Handlebars from 'handlebars'; +import path from 'path'; + +// Load the template +const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/expired-account-activation-link.hbs'), 'utf8'); +const template = Handlebars.compile(templateSource); +const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); +Handlebars.registerPartial('base_template', baseTemplateSource); + +// Sample data to pass to the template +const data = { + title: 'Activation Link Expired', + logoUrl: 'https://example.com/logo.png', + // imageUrl: 'https://example.com/expired-linkt.png', + userName: 'John Doe', + activationLinkUrl: 'https://example.com/activate-account', + companyName: 'Boilerplate', + supportUrl: 'https://example.com/support', + socialIcons: [ + { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, + { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, + { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } + ], + companyWebsite: 'https://example.com', + preferencesUrl: 'https://example.com/preferences', + unsubscribeUrl: 'https://example.com/unsubscribe' +}; + +describe('Email Template', () => { + it('should render correctly with provided data', () => { + const result = template(data); + + // Check for the presence of critical elements + expect(result).toContain(data.title); + expect(result).toContain(data.logoUrl); + // expect(result).toContain(data.imageUrl); + expect(result).toContain(`Hi ${data.userName}`); + expect(result).toContain(data.activationLinkUrl); + expect(result).toContain(data.companyName); + expect(result).toContain(data.supportUrl); + data.socialIcons.forEach(icon => { + expect(result).toContain(icon.url); + expect(result).toContain(icon.imgSrc); + expect(result).toContain(icon.alt); + }); + expect(result).toContain(data.companyWebsite); + expect(result).toContain(data.preferencesUrl); + expect(result).toContain(data.unsubscribeUrl); + }); +}); diff --git a/src/views/email/tests/new-activation-link-sent.spec.ts b/src/views/email/tests/new-activation-link-sent.spec.ts index 4ff475a1..5f44f164 100644 --- a/src/views/email/tests/new-activation-link-sent.spec.ts +++ b/src/views/email/tests/new-activation-link-sent.spec.ts @@ -1,53 +1,53 @@ -import { describe, expect, it, test } from '@jest/globals'; -import fs from 'fs-extra'; -import Handlebars from 'handlebars'; -import path from 'path'; - -const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); -Handlebars.registerPartial('base_template', baseTemplateSource); - -// Load the template -const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/new-activation-link-sent.hbs'), 'utf8'); -const template = Handlebars.compile(templateSource); - -// Sample data to pass to the template -const data = { - title: 'New Activation Link Sent', - logoUrl: 'https://example.com/logo.png', - // imageUrl: 'https://example.com/reset-password.png', - userName: 'John Doe', - activationLinkUrl: 'https://example.com/activate-account', - companyName: 'Boilerplate', - supportUrl: 'https://example.com/support', - socialIcons: [ - { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, - { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, - { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } - ], - companyWebsite: 'https://example.com', - preferencesUrl: 'https://example.com/preferences', - unsubscribeUrl: 'https://example.com/unsubscribe' -}; - -describe('Email Template', () => { - it('should render correctly with provided data', () => { - const result = template(data); - - // Check for the presence of critical elements - expect(result).toContain(data.title); - expect(result).toContain(data.logoUrl); - // expect(result).toContain(data.imageUrl); - expect(result).toContain(`Hi ${data.userName}`); - expect(result).toContain(data.activationLinkUrl); - expect(result).toContain(data.companyName); - expect(result).toContain(data.supportUrl); - data.socialIcons.forEach(icon => { - expect(result).toContain(icon.url); - expect(result).toContain(icon.imgSrc); - expect(result).toContain(icon.alt); - }); - expect(result).toContain(data.companyWebsite); - expect(result).toContain(data.preferencesUrl); - expect(result).toContain(data.unsubscribeUrl); - }); -}); +import { describe, expect, it, test } from '@jest/globals'; +import fs from 'fs-extra'; +import Handlebars from 'handlebars'; +import path from 'path'; + +const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); +Handlebars.registerPartial('base_template', baseTemplateSource); + +// Load the template +const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/new-activation-link-sent.hbs'), 'utf8'); +const template = Handlebars.compile(templateSource); + +// Sample data to pass to the template +const data = { + title: 'New Activation Link Sent', + logoUrl: 'https://example.com/logo.png', + // imageUrl: 'https://example.com/reset-password.png', + userName: 'John Doe', + activationLinkUrl: 'https://example.com/activate-account', + companyName: 'Boilerplate', + supportUrl: 'https://example.com/support', + socialIcons: [ + { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, + { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, + { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } + ], + companyWebsite: 'https://example.com', + preferencesUrl: 'https://example.com/preferences', + unsubscribeUrl: 'https://example.com/unsubscribe' +}; + +describe('Email Template', () => { + it('should render correctly with provided data', () => { + const result = template(data); + + // Check for the presence of critical elements + expect(result).toContain(data.title); + expect(result).toContain(data.logoUrl); + // expect(result).toContain(data.imageUrl); + expect(result).toContain(`Hi ${data.userName}`); + expect(result).toContain(data.activationLinkUrl); + expect(result).toContain(data.companyName); + expect(result).toContain(data.supportUrl); + data.socialIcons.forEach(icon => { + expect(result).toContain(icon.url); + expect(result).toContain(icon.imgSrc); + expect(result).toContain(icon.alt); + }); + expect(result).toContain(data.companyWebsite); + expect(result).toContain(data.preferencesUrl); + expect(result).toContain(data.unsubscribeUrl); + }); +}); diff --git a/src/views/email/tests/password-reset-complete.spec.ts b/src/views/email/tests/password-reset-complete.spec.ts index 73801951..bf68f524 100644 --- a/src/views/email/tests/password-reset-complete.spec.ts +++ b/src/views/email/tests/password-reset-complete.spec.ts @@ -1,52 +1,52 @@ -import { describe, expect, it, test } from '@jest/globals'; -import fs from 'fs-extra'; -import Handlebars from 'handlebars'; -import path from 'path'; - -const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); -Handlebars.registerPartial('base_template', baseTemplateSource); - -// Load the template -const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/password-reset-complete.hbs'), 'utf8'); -const template = Handlebars.compile(templateSource); - -// Sample data to pass to the template -const data = { - title: 'Password Reset Complete', - logoUrl: 'https://example.com/logo.png', - // imageUrl: 'https://example.com/reset-password-complete.png', - userName: 'John Doe', - resetUrl: 'https://example.com/reset-password', - accountRecoverUrl: 'https://example.com/forgot', - supportUrl: 'https://example.com/support', - socialIcons: [ - { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, - { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, - { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } - ], - companyWebsite: 'https://example.com', - preferencesUrl: 'https://example.com/preferences', - unsubscribeUrl: 'https://example.com/unsubscribe' -}; - -describe('Email Template', () => { - it('should render correctly with provided data', () => { - const result = template(data); - - // Check for the presence of critical elements - expect(result).toContain(data.title); - expect(result).toContain(data.logoUrl); - // expect(result).toContain(data.imageUrl); - expect(result).toContain(`Hi ${data.userName}`); - expect(result).toContain(data.accountRecoverUrl); - expect(result).toContain(data.supportUrl); - data.socialIcons.forEach(icon => { - expect(result).toContain(icon.url); - expect(result).toContain(icon.imgSrc); - expect(result).toContain(icon.alt); - }); - expect(result).toContain(data.companyWebsite); - expect(result).toContain(data.preferencesUrl); - expect(result).toContain(data.unsubscribeUrl); - }); -}); +import { describe, expect, it, test } from '@jest/globals'; +import fs from 'fs-extra'; +import Handlebars from 'handlebars'; +import path from 'path'; + +const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); +Handlebars.registerPartial('base_template', baseTemplateSource); + +// Load the template +const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/password-reset-complete.hbs'), 'utf8'); +const template = Handlebars.compile(templateSource); + +// Sample data to pass to the template +const data = { + title: 'Password Reset Complete', + logoUrl: 'https://example.com/logo.png', + // imageUrl: 'https://example.com/reset-password-complete.png', + userName: 'John Doe', + resetUrl: 'https://example.com/reset-password', + accountRecoverUrl: 'https://example.com/forgot', + supportUrl: 'https://example.com/support', + socialIcons: [ + { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, + { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, + { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } + ], + companyWebsite: 'https://example.com', + preferencesUrl: 'https://example.com/preferences', + unsubscribeUrl: 'https://example.com/unsubscribe' +}; + +describe('Email Template', () => { + it('should render correctly with provided data', () => { + const result = template(data); + + // Check for the presence of critical elements + expect(result).toContain(data.title); + expect(result).toContain(data.logoUrl); + // expect(result).toContain(data.imageUrl); + expect(result).toContain(`Hi ${data.userName}`); + expect(result).toContain(data.accountRecoverUrl); + expect(result).toContain(data.supportUrl); + data.socialIcons.forEach(icon => { + expect(result).toContain(icon.url); + expect(result).toContain(icon.imgSrc); + expect(result).toContain(icon.alt); + }); + expect(result).toContain(data.companyWebsite); + expect(result).toContain(data.preferencesUrl); + expect(result).toContain(data.unsubscribeUrl); + }); +}); diff --git a/src/views/email/tests/password-reset.spec.ts b/src/views/email/tests/password-reset.spec.ts index 7a1cfb4c..7e21eb13 100644 --- a/src/views/email/tests/password-reset.spec.ts +++ b/src/views/email/tests/password-reset.spec.ts @@ -1,52 +1,52 @@ -import { describe, expect, it, test } from '@jest/globals'; -import fs from 'fs-extra'; -import Handlebars from 'handlebars'; -import path from 'path'; - -// Load the template -const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/password-reset.hbs'), 'utf8'); -const template = Handlebars.compile(templateSource); -const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); -Handlebars.registerPartial('base_template', baseTemplateSource); - -// Sample data to pass to the template -const data = { - title: 'Reset Your Password', - logoUrl: 'https://example.com/logo.png', - // imageUrl: 'https://example.com/activation-image.png', - userName: 'John Doe', - resetUrl: 'https://example.com/reset-password', - companyName: 'Boilerplate', - supportUrl: 'https://example.com/support', - socialIcons: [ - { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, - { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, - { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } - ], - companyWebsite: 'https://example.com', - preferencesUrl: 'https://example.com/preferences', - unsubscribeUrl: 'https://example.com/unsubscribe' -}; - -describe('Email Template', () => { - it('should render correctly with provided data', () => { - const result = template(data); - - // Check for the presence of critical elements - expect(result).toContain(data.title); - expect(result).toContain(data.logoUrl); - // expect(result).toContain(data.imageUrl); - expect(result).toContain(`Hi ${data.userName}`); - expect(result).toContain(data.resetUrl); - expect(result).toContain(data.companyName); - expect(result).toContain(data.supportUrl); - data.socialIcons.forEach(icon => { - expect(result).toContain(icon.url); - expect(result).toContain(icon.imgSrc); - expect(result).toContain(icon.alt); - }); - expect(result).toContain(data.companyWebsite); - expect(result).toContain(data.preferencesUrl); - expect(result).toContain(data.unsubscribeUrl); - }); -}); +import { describe, expect, it, test } from '@jest/globals'; +import fs from 'fs-extra'; +import Handlebars from 'handlebars'; +import path from 'path'; + +// Load the template +const templateSource = fs.readFileSync(path.resolve('src/views/email/templates/password-reset.hbs'), 'utf8'); +const template = Handlebars.compile(templateSource); +const baseTemplateSource = fs.readFileSync(path.resolve('src/views/email/templates/base_template.hbs'), 'utf8'); +Handlebars.registerPartial('base_template', baseTemplateSource); + +// Sample data to pass to the template +const data = { + title: 'Reset Your Password', + logoUrl: 'https://example.com/logo.png', + // imageUrl: 'https://example.com/activation-image.png', + userName: 'John Doe', + resetUrl: 'https://example.com/reset-password', + companyName: 'Boilerplate', + supportUrl: 'https://example.com/support', + socialIcons: [ + { url: 'https://facebook.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/tiktok@2x.png', alt: 'Facebook' }, + { url: 'https://twitter.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/twitter@2x.png', alt: 'Twitter' }, + { url: 'https://instagram.com', imgSrc: 'https://app-rsrc.getbee.io/public/resources/social-networks-icon-sets/t-only-logo-dark-gray/instagram@2x.png', alt: 'Instagram' } + ], + companyWebsite: 'https://example.com', + preferencesUrl: 'https://example.com/preferences', + unsubscribeUrl: 'https://example.com/unsubscribe' +}; + +describe('Email Template', () => { + it('should render correctly with provided data', () => { + const result = template(data); + + // Check for the presence of critical elements + expect(result).toContain(data.title); + expect(result).toContain(data.logoUrl); + // expect(result).toContain(data.imageUrl); + expect(result).toContain(`Hi ${data.userName}`); + expect(result).toContain(data.resetUrl); + expect(result).toContain(data.companyName); + expect(result).toContain(data.supportUrl); + data.socialIcons.forEach(icon => { + expect(result).toContain(icon.url); + expect(result).toContain(icon.imgSrc); + expect(result).toContain(icon.alt); + }); + expect(result).toContain(data.companyWebsite); + expect(result).toContain(data.preferencesUrl); + expect(result).toContain(data.unsubscribeUrl); + }); +}); diff --git a/src/views/magic-link.email.ts b/src/views/magic-link.email.ts index eeb862ef..4bdfdc25 100644 --- a/src/views/magic-link.email.ts +++ b/src/views/magic-link.email.ts @@ -1,198 +1,198 @@ -import handlebars from "handlebars"; - -const magicLinkEmail = ` - - - - Magic Link Login - - - - - - - - - - - - - - - - -
- - - - -
-

Welcome, {{email}}

-

- Click the link below to log in to your account: -

-

- Log in with this link -

-
-

or copy this link and paste it in your browser.

-

{{magicLinkUrl}}

-
-

This link will expire on {{expirationTime}}.

-

- If you did not request this link, you can safely ignore this - email. -

-
-
- - -`; - -function generateMagicLinkEmail( - magicLinkUrl: string, - userEmail: string, - expiresAt: string = new Date( - Date.now() + 24 * 60 * 60 * 1000, - ).toLocaleString(), -) { - const template = handlebars.compile(magicLinkEmail); - const htmlBody = template({ - magicLinkUrl, - expirationTime: expiresAt, - email: userEmail, - }); - - return htmlBody; -} - -export { generateMagicLinkEmail, magicLinkEmail }; +import handlebars from "handlebars"; + +const magicLinkEmail = ` + + + + Magic Link Login + + + + + + + + + + + + + + + + +
+ + + + +
+

Welcome, {{email}}

+

+ Click the link below to log in to your account: +

+

+ Log in with this link +

+
+

or copy this link and paste it in your browser.

+

{{magicLinkUrl}}

+
+

This link will expire on {{expirationTime}}.

+

+ If you did not request this link, you can safely ignore this + email. +

+
+
+ + +`; + +function generateMagicLinkEmail( + magicLinkUrl: string, + userEmail: string, + expiresAt: string = new Date( + Date.now() + 24 * 60 * 60 * 1000, + ).toLocaleString(), +) { + const template = handlebars.compile(magicLinkEmail); + const htmlBody = template({ + magicLinkUrl, + expirationTime: expiresAt, + email: userEmail, + }); + + return htmlBody; +} + +export { generateMagicLinkEmail, magicLinkEmail }; diff --git a/src/views/welcome.ts b/src/views/welcome.ts index 816337c2..bc12a085 100644 --- a/src/views/welcome.ts +++ b/src/views/welcome.ts @@ -1,1778 +1,1778 @@ -import handlebars from "handlebars"; - -export const welcome = ` - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -export function compilerOtp(otp_code: number, name: string) { - const template = handlebars.compile(welcome); - const htmlBody = template({ - otp: otp_code, - name: name, - }); - return htmlBody; -} +import handlebars from "handlebars"; + +export const welcome = ` + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +export function compilerOtp(otp_code: number, name: string) { + const template = handlebars.compile(welcome); + const htmlBody = template({ + otp: otp_code, + name: name, + }); + return htmlBody; +}