arkos 1.0.1-beta → 1.0.2-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cjs/app.js +1 -1
- package/dist/cjs/app.js.map +1 -1
- package/dist/cjs/exports/services/index.js +3 -3
- package/dist/cjs/exports/services/index.js.map +1 -1
- package/dist/cjs/modules/auth/__tests__/auth.controller.test.js +0 -1
- package/dist/cjs/modules/auth/__tests__/auth.controller.test.js.map +1 -1
- package/dist/cjs/modules/auth/__tests__/auth.service.test.js +470 -0
- package/dist/cjs/modules/auth/__tests__/auth.service.test.js.map +1 -0
- package/dist/cjs/modules/auth/auth.service.js +30 -9
- package/dist/cjs/modules/auth/auth.service.js.map +1 -1
- package/dist/cjs/modules/email/email.service.js +12 -7
- package/dist/cjs/modules/email/email.service.js.map +1 -1
- package/dist/cjs/modules/file-uploader/__tests__/file-uploader.service.test.js +402 -0
- package/dist/cjs/modules/file-uploader/__tests__/file-uploader.service.test.js.map +1 -0
- package/dist/cjs/modules/file-uploader/file-uploader.controller.js +226 -0
- package/dist/cjs/modules/file-uploader/file-uploader.controller.js.map +1 -0
- package/dist/cjs/modules/file-uploader/file-uploader.router.js +50 -0
- package/dist/cjs/modules/file-uploader/file-uploader.router.js.map +1 -0
- package/dist/cjs/modules/file-uploader/file-uploader.service.js +299 -0
- package/dist/cjs/modules/file-uploader/file-uploader.service.js.map +1 -0
- package/dist/cjs/modules/file-uploader/utils/helpers/__tests__/file-uploader.helpers.test.js +164 -0
- package/dist/cjs/modules/file-uploader/utils/helpers/__tests__/file-uploader.helpers.test.js.map +1 -0
- package/dist/cjs/modules/file-uploader/utils/helpers/file-uploader.helpers.js +85 -0
- package/dist/cjs/modules/file-uploader/utils/helpers/file-uploader.helpers.js.map +1 -0
- package/dist/cjs/server.js +1 -1
- package/dist/cjs/server.js.map +1 -1
- package/dist/cjs/types/arkos-config.js.map +1 -1
- package/dist/cjs/types/auth.js.map +1 -1
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/cjs/utils/helpers/models.helpers.js +19 -6
- package/dist/cjs/utils/helpers/models.helpers.js.map +1 -1
- package/dist/es2020/app.js +1 -1
- package/dist/es2020/app.js.map +1 -1
- package/dist/es2020/exports/services/index.js +1 -1
- package/dist/es2020/exports/services/index.js.map +1 -1
- package/dist/es2020/modules/auth/auth.service.js +30 -9
- package/dist/es2020/modules/auth/auth.service.js.map +1 -1
- package/dist/es2020/modules/email/email.service.js +12 -7
- package/dist/es2020/modules/email/email.service.js.map +1 -1
- package/dist/es2020/modules/file-uploader/file-uploader.controller.js +220 -0
- package/dist/es2020/modules/file-uploader/file-uploader.controller.js.map +1 -0
- package/dist/es2020/modules/file-uploader/file-uploader.router.js +44 -0
- package/dist/es2020/modules/file-uploader/file-uploader.router.js.map +1 -0
- package/dist/es2020/modules/file-uploader/file-uploader.service.js +291 -0
- package/dist/es2020/modules/file-uploader/file-uploader.service.js.map +1 -0
- package/dist/es2020/modules/file-uploader/utils/helpers/file-uploader.helpers.js +77 -0
- package/dist/es2020/modules/file-uploader/utils/helpers/file-uploader.helpers.js.map +1 -0
- package/dist/es2020/server.js +1 -1
- package/dist/es2020/server.js.map +1 -1
- package/dist/es2020/types/arkos-config.js.map +1 -1
- package/dist/es2020/types/auth.js.map +1 -1
- package/dist/es2020/types/index.js.map +1 -1
- package/dist/es2020/utils/helpers/models.helpers.js +19 -6
- package/dist/es2020/utils/helpers/models.helpers.js.map +1 -1
- package/dist/types/exports/services/index.d.ts +1 -1
- package/dist/types/modules/email/email.service.d.ts +1 -2
- package/dist/types/modules/file-uploader/file-uploader.controller.d.ts +3 -0
- package/dist/types/modules/file-uploader/file-uploader.router.d.ts +3 -0
- package/dist/types/modules/file-uploader/file-uploader.service.d.ts +30 -0
- package/dist/types/modules/file-uploader/utils/helpers/file-uploader.helpers.d.ts +2 -0
- package/dist/types/types/arkos-config.d.ts +8 -1
- package/dist/types/types/auth.d.ts +2 -2
- package/dist/types/types/index.d.ts +1 -0
- package/package.json +6 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.js","sourceRoot":"","sources":["../../../../src/modules/auth/auth.service.ts"],"names":[],"mappings":"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","sourcesContent":["import jwt from \"jsonwebtoken\";\nimport bcrypt from \"bcryptjs\";\nimport { User, UserRole } from \"../../types\";\nimport catchAsync from \"../error-handler/utils/catch-async\";\nimport AppError from \"../error-handler/utils/app-error\";\nimport { callNext } from \"../base/base.middlewares\";\nimport { getArkosConfig } from \"../../server\";\nimport arkosEnv from \"../../utils/arkos-env\";\nimport { getPrismaInstance } from \"../../utils/helpers/prisma.helpers\";\nimport {\n ArkosRequest,\n ArkosResponse,\n ArkosNextFunction,\n ArkosRequestHandler,\n} from \"../../types\";\nimport {\n AuthConfigs,\n AuthJwtPayload,\n ControllerActions,\n} from \"../../types/auth\";\nimport { kebabCase } from \"../../utils/helpers/change-case.helpers\";\nimport { singular } from \"pluralize\";\n\n/**\n * Handles various authentication-related tasks such as JWT signing, password hashing, and verifying user credentials.\n */\nclass AuthService {\n /**\n * Signs a JWT token for the user.\n *\n * @param {number | string} id - The unique identifier of the user to generate the token for.\n * @param {string | number} [expiresIn] - The expiration time for the token. Defaults to environment variable `JWT_EXPIRES_IN`.\n * @param {string} [secret] - The secret key used to sign the token. Defaults to environment variable `JWT_SECRET`.\n * @returns {string} The signed JWT token.\n */\n signJwtToken(\n id: number | string,\n expiresIn: string | number = process.env.JWT_EXPIRES_IN ||\n arkosEnv.JWT_EXPIRES_IN,\n secret: string = process.env.JWT_SECRET || arkosEnv.JWT_SECRET\n ): string {\n return jwt.sign({ id }, secret, {\n expiresIn: expiresIn as any,\n });\n }\n\n /**\n * Compares a candidate password with the stored user password to check if they match.\n *\n * @param {string} candidatePassword - The password provided by the user during login.\n * @param {string} userPassword - The password stored in the database.\n * @returns {Promise<boolean>} Returns true if the passwords match, otherwise false.\n */\n async isCorrectPassword(\n candidatePassword: string,\n userPassword: string\n ): Promise<boolean> {\n return await bcrypt.compare(candidatePassword, userPassword);\n }\n\n /**\n * Hashes a plain text password using bcrypt.\n *\n * @param {string} password - The password to be hashed.\n * @returns {Promise<string>} Returns the hashed password.\n */\n async hashPassword(password: string): Promise<string> {\n return await bcrypt.hash(password, 12);\n }\n\n /**\n * Checks if a password is strong, requiring uppercase, lowercase, and numeric characters as the default.\n *\n * **Note**: You can define it when calling arkos.init()\n * ```ts\n * arkos.init({\n * authentication: {\n * passwordValidation:{ regex: /your-desired-regex/, message: 'password must contain...'}\n * }\n * })\n * ```\n *\n * @param {string} password - The password to check.\n * @returns {boolean} Returns true if the password meets the strength criteria, otherwise false.\n */\n isPasswordStrong(password: string): boolean {\n const initAuthConfigs = getArkosConfig()?.authentication;\n\n const strongPasswordRegex =\n initAuthConfigs?.passwordValidation?.regex ||\n /^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d).+$/;\n return strongPasswordRegex.test(password);\n }\n\n /**\n * Checks if a user has changed their password after the JWT was issued.\n *\n * @param {User} user - The user object containing the passwordChangedAt field.\n * @param {number} JWTTimestamp - The timestamp when the JWT was issued.\n * @returns {boolean} Returns true if the user changed their password after the JWT was issued, otherwise false.\n */\n userChangedPasswordAfter(user: User, JWTTimestamp: number): boolean {\n if (user.passwordChangedAt) {\n const convertedTimestamp = parseInt(\n String(user.passwordChangedAt.getTime() / 1000),\n 10\n );\n\n return JWTTimestamp < convertedTimestamp;\n }\n return false;\n }\n\n /**\n * Verifies the authenticity of a JWT token.\n *\n * @param {string} token - The JWT token to verify.\n * @param {string} [secret] - The secret key used to verify the token. Defaults to environment variable `JWT_SECRET`.\n * @returns {Promise<AuthJwtPayload>} Returns the decoded JWT payload if the token is valid.\n * @throws {Error} Throws an error if the token is invalid or expired.\n */\n async verifyJwtToken(\n token: string,\n secret: string = process.env.JWT_SECRET || arkosEnv.JWT_SECRET\n ): Promise<AuthJwtPayload> {\n return new Promise((resolve, reject) => {\n jwt.verify(token, secret, (err, decoded) => {\n if (err) reject(err);\n else resolve(decoded as AuthJwtPayload);\n });\n });\n }\n\n /**\n * Middleware function to handle access control based on user roles and permissions.\n *\n * @param {AuthConfigs} authConfigs - The configuration object for authentication and access control.\n * @param {ControllerActions} action - The action being performed (e.g., create, update, delete, view).\n * @param {string} modelName - The model name that the action is being performed on (e.g., \"User\", \"Post\").\n * @returns {ArkosRequestHandler} The middleware function that checks if the user has permission to perform the action.\n */\n handleActionAccessControl(\n authConfigs: AuthConfigs,\n action: ControllerActions,\n modelName: string\n ): ArkosRequestHandler {\n return catchAsync(\n async (\n req: ArkosRequest,\n res: ArkosResponse,\n next: ArkosNextFunction\n ) => {\n if (req.user) {\n const user = req.user as any;\n const prisma = getPrismaInstance();\n\n const permissions = await (prisma as any).authPermission.count({\n where: {\n resource: kebabCase(singular(modelName)),\n action,\n roleId: { in: user.roles.map((role: UserRole) => role.roleId) },\n },\n });\n\n if (!permissions) {\n return next(\n new AppError(\n \"You do not have permission to perfom this action\",\n 403\n )\n );\n }\n }\n\n next();\n }\n );\n }\n\n /**\n * Processes the cookies or authoriation token and returns the user.\n * @param req\n * @returns {Promise<User | null>} - if authentication is turned off in arkosConfig it returns null\n * @throws {AppError} Throws an error if the token is invalid or the user is not logged in.\n */\n async getAuthenticatedUser(req: ArkosRequest): Promise<User | null> {\n const arkosConfig = getArkosConfig();\n if (!arkosConfig?.authentication) return null;\n\n const prisma = getPrismaInstance();\n\n let token: string | undefined;\n\n if (\n req?.headers?.authorization &&\n req?.headers?.authorization.startsWith(\"Bearer\")\n ) {\n token = req?.headers?.authorization.split(\" \")[1];\n } else if (req?.cookies?.arkos_access_token !== \"no-token\" && req.cookies) {\n token = req?.cookies?.arkos_access_token;\n }\n\n if (!token)\n throw new AppError(\n \"You are not logged in! please log in to get access\",\n 401\n );\n\n let decoded: AuthJwtPayload | undefined;\n try {\n decoded = await this.verifyJwtToken(token);\n } catch (err) {\n throw new AppError(\n \"Your auth token is invalid, please login again.\",\n 401\n );\n }\n\n if (!decoded?.id)\n throw new AppError(\n \"Your auth token is invalid, please login again.\",\n 401\n );\n\n const user: any | null = await (prisma as any).user.findUnique({\n where: { id: String(decoded.id) },\n include: {\n roles: {\n include: {\n role: {\n include: {\n permissions: true,\n },\n },\n },\n },\n },\n });\n\n if (!user)\n throw new AppError(\n \"The user belonging to this token does no longer exists\",\n 401\n );\n\n if (\n this.userChangedPasswordAfter(user, decoded.iat!) &&\n !req.path.includes(\"logout\")\n )\n throw new AppError(\n \"User recently changed password! Please log in again.\",\n 401\n );\n\n return user;\n }\n\n /**\n * Middleware function to authenticate the user based on the JWT token.\n *\n * @param {ArkosRequest} req - The request object.\n * @param {ArkosResponse} res - The response object.\n * @param {ArkosNextFunction} next - The next middleware function to be called.\n * @returns {void}\n */\n authenticate = catchAsync(\n async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {\n const arkosConfig = getArkosConfig();\n if (!arkosConfig?.authentication) {\n next();\n return;\n }\n\n req.user = (await this.getAuthenticatedUser(req)) as User;\n next();\n }\n );\n\n /**\n * Handles authentication control by checking the `authenticationControl` configuration in the `authConfigs`.\n *\n * @param {AuthConfigs | undefined} authConfigs - The authentication configuration object.\n * @param {ControllerActions} action - The action being performed (e.g., create, update, delete, view).\n * @param {string} modelName - The model name being affected by the action.\n * @returns {ArkosRequestHandler} The middleware function that checks if authentication is required.\n */\n handleAuthenticationControl(\n authConfigs: AuthConfigs | undefined,\n action: ControllerActions,\n modelName: string\n ): ArkosRequestHandler {\n const authenticationControl = authConfigs?.authenticationControl;\n\n if (authenticationControl && typeof authenticationControl === \"object\") {\n if (authenticationControl[action] === false) return callNext;\n else if (authenticationControl[action] === true) return this.authenticate;\n } else return this.authenticate;\n\n return this.authenticate;\n }\n}\n\n/**\n * Handles various authentication-related tasks such as JWT signing, password hashing, and verifying user credentials.\n */\nconst authService = new AuthService();\n\nexport default authService;\n"]}
|
|
1
|
+
{"version":3,"file":"auth.service.js","sourceRoot":"","sources":["../../../../src/modules/auth/auth.service.ts"],"names":[],"mappings":"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","sourcesContent":["import jwt from \"jsonwebtoken\";\nimport bcrypt from \"bcryptjs\";\nimport { User, UserRole } from \"../../types\";\nimport catchAsync from \"../error-handler/utils/catch-async\";\nimport AppError from \"../error-handler/utils/app-error\";\nimport { callNext } from \"../base/base.middlewares\";\nimport { getArkosConfig } from \"../../server\";\nimport arkosEnv from \"../../utils/arkos-env\";\nimport { getPrismaInstance } from \"../../utils/helpers/prisma.helpers\";\nimport {\n ArkosRequest,\n ArkosResponse,\n ArkosNextFunction,\n ArkosRequestHandler,\n} from \"../../types\";\nimport {\n AuthConfigs,\n AuthJwtPayload,\n ControllerActions,\n} from \"../../types/auth\";\nimport { kebabCase } from \"../../utils/helpers/change-case.helpers\";\nimport { singular } from \"pluralize\";\n\n/**\n * Handles various authentication-related tasks such as JWT signing, password hashing, and verifying user credentials.\n */\nclass AuthService {\n /**\n * Signs a JWT token for the user.\n *\n * @param {number | string} id - The unique identifier of the user to generate the token for.\n * @param {string | number} [expiresIn] - The expiration time for the token. Defaults to environment variable `JWT_EXPIRES_IN`.\n * @param {string} [secret] - The secret key used to sign the token. Defaults to environment variable `JWT_SECRET`.\n * @returns {string} The signed JWT token.\n */\n signJwtToken(\n id: number | string,\n expiresIn: string | number = process.env.JWT_EXPIRES_IN ||\n arkosEnv.JWT_EXPIRES_IN,\n secret: string = process.env.JWT_SECRET || arkosEnv.JWT_SECRET\n ): string {\n return jwt.sign({ id }, secret, {\n expiresIn: expiresIn as any,\n });\n }\n\n /**\n * Compares a candidate password with the stored user password to check if they match.\n *\n * @param {string} candidatePassword - The password provided by the user during login.\n * @param {string} userPassword - The password stored in the database.\n * @returns {Promise<boolean>} Returns true if the passwords match, otherwise false.\n */\n async isCorrectPassword(\n candidatePassword: string,\n userPassword: string\n ): Promise<boolean> {\n return await bcrypt.compare(candidatePassword, userPassword);\n }\n\n /**\n * Hashes a plain text password using bcrypt.\n *\n * @param {string} password - The password to be hashed.\n * @returns {Promise<string>} Returns the hashed password.\n */\n async hashPassword(password: string): Promise<string> {\n return await bcrypt.hash(password, 12);\n }\n\n /**\n * Checks if a password is strong, requiring uppercase, lowercase, and numeric characters as the default.\n *\n * **Note**: You can define it when calling arkos.init()\n * ```ts\n * arkos.init({\n * authentication: {\n * passwordValidation:{ regex: /your-desired-regex/, message: 'password must contain...'}\n * }\n * })\n * ```\n *\n * @param {string} password - The password to check.\n * @returns {boolean} Returns true if the password meets the strength criteria, otherwise false.\n */\n isPasswordStrong(password: string): boolean {\n const initAuthConfigs = getArkosConfig()?.authentication;\n\n const strongPasswordRegex =\n initAuthConfigs?.passwordValidation?.regex ||\n /^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d).+$/;\n return strongPasswordRegex.test(password);\n }\n\n /**\n * Checks if a user has changed their password after the JWT was issued.\n *\n * @param {User} user - The user object containing the passwordChangedAt field.\n * @param {number} JWTTimestamp - The timestamp when the JWT was issued.\n * @returns {boolean} Returns true if the user changed their password after the JWT was issued, otherwise false.\n */\n userChangedPasswordAfter(user: User, JWTTimestamp: number): boolean {\n if (user.passwordChangedAt) {\n const convertedTimestamp = parseInt(\n String(user.passwordChangedAt.getTime() / 1000),\n 10\n );\n\n return JWTTimestamp < convertedTimestamp;\n }\n return false;\n }\n\n /**\n * Verifies the authenticity of a JWT token.\n *\n * @param {string} token - The JWT token to verify.\n * @param {string} [secret] - The secret key used to verify the token. Defaults to environment variable `JWT_SECRET`.\n * @returns {Promise<AuthJwtPayload>} Returns the decoded JWT payload if the token is valid.\n * @throws {Error} Throws an error if the token is invalid or expired.\n */\n async verifyJwtToken(\n token: string,\n secret: string = process.env.JWT_SECRET || arkosEnv.JWT_SECRET\n ): Promise<AuthJwtPayload> {\n return new Promise((resolve, reject) => {\n jwt.verify(token, secret, (err, decoded) => {\n if (err) reject(err);\n else resolve(decoded as AuthJwtPayload);\n });\n });\n }\n\n /**\n * Middleware function to handle access control based on user roles and permissions.\n *\n * @param {AuthConfigs} authConfigs - The configuration object for authentication and access control.\n * @param {ControllerActions} action - The action being performed (e.g., create, update, delete, view).\n * @param {string} modelName - The model name that the action is being performed on (e.g., \"User\", \"Post\").\n * @returns {ArkosRequestHandler} The middleware function that checks if the user has permission to perform the action.\n */\n handleActionAccessControl(\n authConfigs: AuthConfigs,\n action: ControllerActions,\n modelName: string\n ): ArkosRequestHandler {\n return catchAsync(\n async (\n req: ArkosRequest,\n res: ArkosResponse,\n next: ArkosNextFunction\n ) => {\n if (req.user) {\n const user = req.user as any;\n const prisma = getPrismaInstance();\n const configs = getArkosConfig();\n\n if (user.isSuperUser) {\n next();\n return;\n }\n\n if (configs?.authentication?.mode === \"dynamic\") {\n const permissions = await (prisma as any).authPermission.count({\n where: {\n resource: kebabCase(singular(modelName)),\n action,\n roleId: { in: user.roles.map((role: UserRole) => role.roleId) },\n },\n });\n\n if (!permissions)\n return next(\n new AppError(\n \"You do not have permission to perfom this action\",\n 403\n )\n );\n } else if (configs?.authentication?.mode === \"static\") {\n const accessControl = authConfigs?.accessControl;\n let authorizedRoles: any[] = [];\n\n if (accessControl) {\n if (Array.isArray(accessControl)) authorizedRoles = accessControl;\n else if (accessControl[action])\n authorizedRoles = accessControl[action];\n\n // Checks for both cases if using single role or multiple roles\n if (\n !authorizedRoles.includes(req.user?.role) ||\n ((req.user?.roles as any[]).length > 0 &&\n !authorizedRoles?.some((role) =>\n (req.user?.roles as any[]).includes(role)\n ))\n )\n return next(\n new AppError(\n \"You do not have permission to perfom this action\",\n 403\n )\n );\n }\n }\n }\n\n next();\n }\n );\n }\n\n /**\n * Processes the cookies or authoriation token and returns the user.\n * @param req\n * @returns {Promise<User | null>} - if authentication is turned off in arkosConfig it returns null\n * @throws {AppError} Throws an error if the token is invalid or the user is not logged in.\n */\n async getAuthenticatedUser(req: ArkosRequest): Promise<User | null> {\n const arkosConfig = getArkosConfig();\n if (!arkosConfig?.authentication) return null;\n\n const prisma = getPrismaInstance();\n\n let token: string | undefined;\n\n if (\n req?.headers?.authorization &&\n req?.headers?.authorization.startsWith(\"Bearer\")\n ) {\n token = req?.headers?.authorization.split(\" \")[1];\n } else if (req?.cookies?.arkos_access_token !== \"no-token\" && req.cookies) {\n token = req?.cookies?.arkos_access_token;\n }\n\n if (!token)\n throw new AppError(\n \"You are not logged in! please log in to get access\",\n 401\n );\n\n let decoded: AuthJwtPayload | undefined;\n try {\n decoded = await this.verifyJwtToken(token);\n } catch (err) {\n throw new AppError(\n \"Your auth token is invalid, please login again.\",\n 401\n );\n }\n\n if (!decoded?.id)\n throw new AppError(\n \"Your auth token is invalid, please login again.\",\n 401\n );\n\n const user: any | null = await (prisma as any).user.findUnique({\n where: { id: String(decoded.id) },\n include: {\n roles: {\n include: {\n role: {\n include: {\n permissions: true,\n },\n },\n },\n },\n },\n });\n\n if (!user)\n throw new AppError(\n \"The user belonging to this token does no longer exists\",\n 401\n );\n\n if (\n this.userChangedPasswordAfter(user, decoded.iat!) &&\n !req.path.includes(\"logout\")\n )\n throw new AppError(\n \"User recently changed password! Please log in again.\",\n 401\n );\n\n return user;\n }\n\n /**\n * Middleware function to authenticate the user based on the JWT token.\n *\n * @param {ArkosRequest} req - The request object.\n * @param {ArkosResponse} res - The response object.\n * @param {ArkosNextFunction} next - The next middleware function to be called.\n * @returns {void}\n */\n authenticate = catchAsync(\n async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {\n const arkosConfig = getArkosConfig();\n if (!arkosConfig?.authentication) {\n next();\n return;\n }\n\n req.user = (await this.getAuthenticatedUser(req)) as User;\n next();\n }\n );\n\n /**\n * Handles authentication control by checking the `authenticationControl` configuration in the `authConfigs`.\n *\n * @param {AuthConfigs | undefined} authConfigs - The authentication configuration object.\n * @param {ControllerActions} action - The action being performed (e.g., create, update, delete, view).\n * @param {string} modelName - The model name being affected by the action.\n * @returns {ArkosRequestHandler} The middleware function that checks if authentication is required.\n */\n handleAuthenticationControl(\n authConfigs: AuthConfigs | undefined,\n action: ControllerActions,\n modelName: string\n ): ArkosRequestHandler {\n const authenticationControl = authConfigs?.authenticationControl;\n\n if (authenticationControl && typeof authenticationControl === \"object\") {\n if (authenticationControl[action] === false) return callNext;\n else if (authenticationControl[action] === true) return this.authenticate;\n } else return this.authenticate;\n\n return this.authenticate;\n }\n}\n\n/**\n * Handles various authentication-related tasks such as JWT signing, password hashing, and verifying user credentials.\n */\nconst authService = new AuthService();\n\nexport default authService;\n"]}
|
|
@@ -14,14 +14,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.EmailService = void 0;
|
|
16
16
|
const nodemailer_1 = __importDefault(require("nodemailer"));
|
|
17
|
-
const base_service_1 = require("../base/base.service");
|
|
18
17
|
const html_to_text_1 = require("html-to-text");
|
|
19
|
-
|
|
18
|
+
const server_1 = require("../../server");
|
|
19
|
+
const app_error_1 = __importDefault(require("../error-handler/utils/app-error"));
|
|
20
|
+
class EmailService {
|
|
20
21
|
constructor(host = process.env.EMAIL_HOST, auth = {
|
|
21
22
|
user: process.env.EMAIL_FROM,
|
|
22
23
|
pass: process.env.EMAIL_PASSWORD,
|
|
23
24
|
}, port = parseInt(process.env.EMAIL_PORT || "465")) {
|
|
24
|
-
super("email");
|
|
25
25
|
this.defaultHost = host;
|
|
26
26
|
this.defaultPort = port;
|
|
27
27
|
this.defaultAuth = auth;
|
|
@@ -92,10 +92,15 @@ class EmailService extends base_service_1.BaseService {
|
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
static create(options) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
const { email: emailConfigs } = (0, server_1.getArkosConfig)();
|
|
96
|
+
if (!emailConfigs)
|
|
97
|
+
throw new app_error_1.default("You are trying to use emailService without setting arkosConfig.email configurations", 500, {
|
|
98
|
+
docs: "Read more about emailService at https://www.arkosjs.com/docs/core-concepts/sending-emails",
|
|
99
|
+
});
|
|
100
|
+
return new EmailService(options.host || emailConfigs.host, options.auth || {
|
|
101
|
+
user: emailConfigs.from,
|
|
102
|
+
pass: emailConfigs.password,
|
|
103
|
+
}, options.port || parseInt(String(emailConfigs.port) || "465"));
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
exports.EmailService = EmailService;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email.service.js","sourceRoot":"","sources":["../../../../src/modules/email/email.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,4DAAqD;
|
|
1
|
+
{"version":3,"file":"email.service.js","sourceRoot":"","sources":["../../../../src/modules/email/email.service.ts"],"names":[],"mappings":"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","sourcesContent":["import nodemailer, { Transporter } from \"nodemailer\";\nimport { BaseService } from \"../base/base.service\";\nimport { convert } from \"html-to-text\";\nimport { getArkosConfig } from \"../../server\";\nimport AppError from \"../error-handler/utils/app-error\";\n\n/**\n * Defines the options for sending an email.\n */\nexport type EmailOptions = {\n from?: string; // Sender's email address (optional).\n to: string | string[]; // Recipient's email address or an array of email addresses.\n subject: string; // Subject of the email.\n text?: string; // Plain text body of the email (optional).\n html: string; // HTML body of the email.\n};\n\n/**\n * Defines the authentication options for SMTP.\n */\nexport type SMTPAuthOptions = {\n user: string;\n pass: string;\n};\n\n/**\n * Defines the connection options for SMTP server.\n */\nexport type SMTPConnectionOptions = {\n host?: string;\n port?: number;\n secure?: boolean;\n auth?: SMTPAuthOptions;\n};\n\n/**\n * A service class to handle email-related tasks, including sending emails.\n */\nexport class EmailService {\n private transporter: Transporter;\n private defaultHost: string;\n private defaultPort: number;\n private defaultAuth: SMTPAuthOptions;\n\n /**\n * Creates an instance of the EmailService class.\n *\n * @param {string} [host] - The SMTP host (defaults to the environment variable `EMAIL_HOST`).\n * @param {SMTPAuthOptions} [auth] - The authentication object containing `user` and `pass` for the email account.\n * @param {number} [port] - The SMTP port (defaults to 465).\n *\n * See the api reference [www.arkosjs.com/docs/api-reference/the-email-service-class](https://www.arkosjs.com/docs/api-reference/the-email-service-class)\n */\n constructor(\n host: string = process.env.EMAIL_HOST!,\n auth: SMTPAuthOptions = {\n user: process.env.EMAIL_FROM!,\n pass: process.env.EMAIL_PASSWORD!,\n },\n port: number = parseInt(process.env.EMAIL_PORT || \"465\")\n ) {\n this.defaultHost = host;\n this.defaultPort = port;\n this.defaultAuth = auth;\n\n // Initialize the default transporter\n this.transporter = nodemailer.createTransport({\n host: this.defaultHost,\n port: this.defaultPort,\n secure: true,\n auth: this.defaultAuth,\n });\n }\n\n /**\n * Verifies the connection to the email server.\n * @param {Transporter} [transporterToVerify] - Optional transporter to verify instead of the default one.\n * @returns {Promise<boolean>} A promise that resolves to true if connection is valid.\n */\n public async verifyConnection(\n transporterToVerify?: Transporter\n ): Promise<boolean> {\n try {\n const transporter = transporterToVerify || this.transporter;\n await transporter.verify();\n return true;\n } catch (error) {\n console.error(\"Email Server Connection Failed\", 500);\n return false;\n }\n }\n\n /**\n * Sends an email with the provided options.\n * Can use either the default configuration or custom connection options.\n *\n * @param {EmailOptions} options - The options for the email to be sent.\n * @param {SMTPConnectionOptions} [connectionOptions] - Optional custom connection settings.\n * @returns {Promise<{ success: boolean; messageId?: string; error?: any }>} A promise that resolves with the result of the email send attempt.\n * @throws {Error} Throws an error if the email sending fails.\n */\n public async send(\n options: EmailOptions,\n connectionOptions?: SMTPConnectionOptions\n ): Promise<{ success: boolean; messageId?: string; error?: any }> {\n try {\n let transporter = this.transporter;\n let fromAddress = options?.from || process.env.EMAIL_FROM;\n\n // If custom connection options are provided, create a temporary transporter\n if (connectionOptions) {\n const tempTransporter = nodemailer.createTransport({\n host: connectionOptions.host || this.defaultHost,\n port: connectionOptions.port || this.defaultPort,\n secure:\n connectionOptions.secure !== undefined\n ? connectionOptions.secure\n : true,\n auth: connectionOptions.auth || this.defaultAuth,\n });\n\n // Verify the temporary connection\n await this.verifyConnection(tempTransporter);\n\n // Use the temporary transporter and update from address if auth user is provided\n transporter = tempTransporter;\n if (connectionOptions.auth?.user) {\n fromAddress = options?.from || connectionOptions.auth.user;\n }\n } else {\n // Verify the default connection\n await this.verifyConnection();\n }\n\n // Send the email\n const info = await transporter.sendMail({\n ...options,\n from: fromAddress,\n text: options?.text || convert(options.html),\n });\n\n return { success: true, messageId: info.messageId };\n } catch (error) {\n throw error;\n }\n }\n\n /**\n * Updates the default configuration for the email service.\n *\n * @param {SMTPConnectionOptions} options - The new connection options.\n */\n public updateDefaultConfig(options: SMTPConnectionOptions): void {\n if (options.host) this.defaultHost = options.host;\n if (options.port) this.defaultPort = options.port;\n if (options.auth) this.defaultAuth = options.auth;\n\n // Update the default transporter\n this.transporter = nodemailer.createTransport({\n host: this.defaultHost,\n port: this.defaultPort,\n secure: options.secure !== undefined ? options.secure : true,\n auth: this.defaultAuth,\n });\n }\n\n /**\n * Creates a new instance of EmailService with custom configuration.\n *\n * @param {SMTPConnectionOptions} options - The connection options.\n * @returns {EmailService} A new email service instance.\n */\n public static create(options: SMTPConnectionOptions): EmailService {\n const { email: emailConfigs } = getArkosConfig();\n\n if (!emailConfigs)\n throw new AppError(\n \"You are trying to use emailService without setting arkosConfig.email configurations\",\n 500,\n {\n docs: \"Read more about emailService at https://www.arkosjs.com/docs/core-concepts/sending-emails\",\n }\n );\n\n return new EmailService(\n options.host || emailConfigs.host,\n options.auth || {\n user: emailConfigs.from,\n pass: emailConfigs.password,\n },\n options.port || parseInt(String(emailConfigs.port) || \"465\")\n );\n }\n}\n\n// Create default instance\nconst emailService = new EmailService();\nexport default emailService;\n"]}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const multer_1 = __importDefault(require("multer"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const util_1 = require("util");
|
|
19
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
20
|
+
const file_uploader_service_1 = require("../file-uploader.service");
|
|
21
|
+
const app_error_1 = __importDefault(require("../../error-handler/utils/app-error"));
|
|
22
|
+
const server_1 = require("../../../server");
|
|
23
|
+
jest.mock("multer");
|
|
24
|
+
jest.mock("path");
|
|
25
|
+
jest.mock("fs");
|
|
26
|
+
jest.mock("sharp");
|
|
27
|
+
jest.mock("util");
|
|
28
|
+
jest.mock("../../../server");
|
|
29
|
+
jest.mock("../../error-handler/utils/app-error");
|
|
30
|
+
jest.mock("../../../utils/helpers/prisma.helpers");
|
|
31
|
+
jest.mock("../../../utils/helpers/models.helpers");
|
|
32
|
+
describe("FileUploaderService", () => {
|
|
33
|
+
let mockReq;
|
|
34
|
+
let mockRes;
|
|
35
|
+
let mockNext;
|
|
36
|
+
let fileUploaderService;
|
|
37
|
+
let mockStorage;
|
|
38
|
+
let mockUploader;
|
|
39
|
+
let mockSharp;
|
|
40
|
+
let mockUnlink = jest.fn().mockResolvedValue(undefined);
|
|
41
|
+
let mockStat = jest.fn().mockResolvedValue({ isFile: () => true });
|
|
42
|
+
let mockRename = jest.fn().mockResolvedValue(undefined);
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
jest.resetAllMocks();
|
|
45
|
+
mockReq = {
|
|
46
|
+
get: jest.fn((key) => {
|
|
47
|
+
if (key === "host")
|
|
48
|
+
return "localhost:3000";
|
|
49
|
+
return null;
|
|
50
|
+
}),
|
|
51
|
+
query: {},
|
|
52
|
+
files: [],
|
|
53
|
+
file: null,
|
|
54
|
+
};
|
|
55
|
+
mockRes = {
|
|
56
|
+
status: jest.fn().mockReturnThis(),
|
|
57
|
+
json: jest.fn(),
|
|
58
|
+
};
|
|
59
|
+
mockNext = jest.fn();
|
|
60
|
+
fs_1.default.existsSync.mockReturnValue(false);
|
|
61
|
+
fs_1.default.mkdirSync.mockReturnValue(undefined);
|
|
62
|
+
const mockPromisify = jest.fn((fn) => fn);
|
|
63
|
+
util_1.promisify.mockImplementation(mockPromisify);
|
|
64
|
+
fs_1.default.stat = jest
|
|
65
|
+
.fn()
|
|
66
|
+
.mockReturnValue({ isFile: () => true });
|
|
67
|
+
fs_1.default.unlink = jest.fn();
|
|
68
|
+
fs_1.default.access = jest
|
|
69
|
+
.fn()
|
|
70
|
+
.mockReturnValue({ isFile: () => true });
|
|
71
|
+
mockUnlink = jest.fn().mockResolvedValue(undefined);
|
|
72
|
+
mockStat = jest.fn().mockResolvedValue({ isFile: () => true });
|
|
73
|
+
util_1.promisify.mockImplementation((fn) => {
|
|
74
|
+
if (fn === fs_1.default.unlink)
|
|
75
|
+
return mockUnlink;
|
|
76
|
+
if (fn === fs_1.default.stat)
|
|
77
|
+
return mockStat;
|
|
78
|
+
if (fn === fs_1.default.rename)
|
|
79
|
+
return mockRename;
|
|
80
|
+
return jest.fn();
|
|
81
|
+
});
|
|
82
|
+
path_1.default.join.mockImplementation((...args) => args.join("/"));
|
|
83
|
+
path_1.default.extname.mockImplementation((filePath) => ".jpg");
|
|
84
|
+
path_1.default.basename.mockImplementation((filePath, ext) => {
|
|
85
|
+
return ext ? filePath.replace(ext, "") : filePath;
|
|
86
|
+
});
|
|
87
|
+
path_1.default.dirname.mockImplementation((filePath) => {
|
|
88
|
+
const parts = filePath.split("/");
|
|
89
|
+
return parts.slice(0, -1).join("/");
|
|
90
|
+
});
|
|
91
|
+
mockStorage = {
|
|
92
|
+
destination: jest.fn(),
|
|
93
|
+
filename: jest.fn(),
|
|
94
|
+
};
|
|
95
|
+
multer_1.default.diskStorage.mockReturnValue(mockStorage);
|
|
96
|
+
mockUploader = {
|
|
97
|
+
single: jest
|
|
98
|
+
.fn()
|
|
99
|
+
.mockReturnValue((req, res, next) => next()),
|
|
100
|
+
array: jest
|
|
101
|
+
.fn()
|
|
102
|
+
.mockReturnValue((req, res, next) => next()),
|
|
103
|
+
};
|
|
104
|
+
multer_1.default.mockReturnValue(mockUploader);
|
|
105
|
+
server_1.getArkosConfig.mockReturnValue({
|
|
106
|
+
fileUpload: {
|
|
107
|
+
baseRoute: "/api/uploads",
|
|
108
|
+
baseUploadDir: "/uploads",
|
|
109
|
+
restrictions: {
|
|
110
|
+
images: {
|
|
111
|
+
maxCount: 30,
|
|
112
|
+
maxSize: 15 * 1024 * 1024,
|
|
113
|
+
supportedFilesRegex: /jpeg|jpg|png/,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
mockSharp = {
|
|
119
|
+
metadata: jest.fn().mockResolvedValue({ width: 800, height: 600 }),
|
|
120
|
+
resize: jest.fn().mockReturnThis(),
|
|
121
|
+
toFormat: jest.fn().mockReturnThis(),
|
|
122
|
+
toFile: jest.fn().mockResolvedValue({}),
|
|
123
|
+
};
|
|
124
|
+
sharp_1.default.mockReturnValue(mockSharp);
|
|
125
|
+
fileUploaderService = new file_uploader_service_1.FileUploaderService("uploads/images", 5 * 1024 * 1024, /jpeg|jpg|png/, 30);
|
|
126
|
+
});
|
|
127
|
+
describe("constructor", () => {
|
|
128
|
+
it("should initialize with default parameters when not provided", () => {
|
|
129
|
+
const service = new file_uploader_service_1.FileUploaderService("uploads/images");
|
|
130
|
+
expect(service["uploadDir"]).toBe("./uploads/images/");
|
|
131
|
+
expect(service["fileSizeLimit"]).toBe(5 * 1024 * 1024);
|
|
132
|
+
expect(service["allowedFileTypes"]).toEqual(/.*/);
|
|
133
|
+
expect(service["maxCount"]).toBe(30);
|
|
134
|
+
});
|
|
135
|
+
it("should create the upload directory if it doesn't exist", () => {
|
|
136
|
+
expect(fs_1.default.existsSync).toHaveBeenCalledWith("./uploads/images/");
|
|
137
|
+
expect(fs_1.default.mkdirSync).toHaveBeenCalledWith("./uploads/images/", {
|
|
138
|
+
recursive: true,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
it("should configure multer storage with correct destination and filename", () => {
|
|
142
|
+
const mockDestinationFn = jest.fn();
|
|
143
|
+
const mockFilenameFn = jest.fn();
|
|
144
|
+
multer_1.default.diskStorage.mockReturnValue({
|
|
145
|
+
destination: mockDestinationFn,
|
|
146
|
+
filename: mockFilenameFn,
|
|
147
|
+
});
|
|
148
|
+
new file_uploader_service_1.FileUploaderService("uploads/images");
|
|
149
|
+
const destinationCallback = multer_1.default.diskStorage.mock
|
|
150
|
+
.calls[0][0].destination;
|
|
151
|
+
const cb = jest.fn();
|
|
152
|
+
destinationCallback({}, {}, cb);
|
|
153
|
+
expect(cb).toHaveBeenCalledWith(null, "./uploads/images/");
|
|
154
|
+
const filenameCallback = multer_1.default.diskStorage.mock
|
|
155
|
+
.calls[0][0].filename;
|
|
156
|
+
const fileCb = jest.fn();
|
|
157
|
+
const mockFile = { originalname: "test.jpg" };
|
|
158
|
+
filenameCallback({}, mockFile, fileCb);
|
|
159
|
+
expect(fileCb).toHaveBeenCalledWith(null, expect.stringMatching(/\d+-\d+\.jpg/));
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe("fileFilter", () => {
|
|
163
|
+
it("should accept valid file types", () => {
|
|
164
|
+
const fileFilter = fileUploaderService["fileFilter"];
|
|
165
|
+
const cb = jest.fn();
|
|
166
|
+
const mockFile = {
|
|
167
|
+
originalname: "test.jpg",
|
|
168
|
+
mimetype: "image/jpeg",
|
|
169
|
+
};
|
|
170
|
+
fileFilter({}, mockFile, cb);
|
|
171
|
+
expect(cb).toHaveBeenCalledWith(null, true);
|
|
172
|
+
});
|
|
173
|
+
it("should reject invalid file types", () => {
|
|
174
|
+
fileUploaderService["allowedFileTypes"] = /jpeg|jpg|png/;
|
|
175
|
+
const fileFilter = fileUploaderService["fileFilter"];
|
|
176
|
+
const cb = jest.fn();
|
|
177
|
+
const mockFile = {
|
|
178
|
+
originalname: "test.pdf",
|
|
179
|
+
mimetype: "application/pdf",
|
|
180
|
+
};
|
|
181
|
+
path_1.default.extname.mockReturnValueOnce(".pdf");
|
|
182
|
+
fileFilter({}, mockFile, cb);
|
|
183
|
+
expect(cb).toHaveBeenCalledWith(expect.any(app_error_1.default));
|
|
184
|
+
expect(app_error_1.default).toHaveBeenCalledWith("Invalid file type", 400);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe("getUploader", () => {
|
|
188
|
+
it("should return multer instance with correct configuration", () => {
|
|
189
|
+
const uploader = fileUploaderService.getUploader();
|
|
190
|
+
expect(multer_1.default).toHaveBeenCalledWith({
|
|
191
|
+
storage: mockStorage,
|
|
192
|
+
fileFilter: expect.any(Function),
|
|
193
|
+
limits: { fileSize: 5 * 1024 * 1024 },
|
|
194
|
+
});
|
|
195
|
+
expect(uploader).toBe(mockUploader);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
describe("handleSingleUpload", () => {
|
|
199
|
+
it("should call next() if upload is successful", () => {
|
|
200
|
+
const middleware = fileUploaderService.handleSingleUpload();
|
|
201
|
+
middleware(mockReq, mockRes, mockNext);
|
|
202
|
+
expect(mockUploader.single).toHaveBeenCalledWith(expect.any(String));
|
|
203
|
+
expect(mockNext).toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
it("should delete old file if oldFilePath is provided", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
|
+
mockUploader.single.mockReturnValueOnce((req, res, next) => next());
|
|
207
|
+
const middleware = fileUploaderService.handleSingleUpload("old22/path/to/file.jpg");
|
|
208
|
+
yield middleware(mockReq, mockRes, () => { });
|
|
209
|
+
expect(mockStat).toHaveBeenCalledWith("old22/path/to/file.jpg");
|
|
210
|
+
expect(mockUnlink).toHaveBeenCalledWith("old22/path/to/file.jpg");
|
|
211
|
+
}));
|
|
212
|
+
it("should pass multer errors to next", () => {
|
|
213
|
+
const multerError = new multer_1.default.MulterError("LIMIT_FILE_SIZE", "fieldname");
|
|
214
|
+
mockUploader.single.mockReturnValueOnce((req, res, next) => next(multerError));
|
|
215
|
+
const middleware = fileUploaderService.handleSingleUpload();
|
|
216
|
+
middleware(mockReq, mockRes, mockNext);
|
|
217
|
+
expect(mockNext).toHaveBeenCalledWith(multerError);
|
|
218
|
+
});
|
|
219
|
+
it("should pass regular errors to next", () => {
|
|
220
|
+
const regularError = new Error("Some other error");
|
|
221
|
+
mockUploader.single.mockReturnValueOnce((req, res, next) => next(regularError));
|
|
222
|
+
const middleware = fileUploaderService.handleSingleUpload();
|
|
223
|
+
middleware(mockReq, mockRes, mockNext);
|
|
224
|
+
expect(mockNext).toHaveBeenCalledWith(regularError);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
describe("handleMultipleUpload", () => {
|
|
228
|
+
it("should call next() if multiple upload is successful", () => {
|
|
229
|
+
const middleware = fileUploaderService.handleMultipleUpload();
|
|
230
|
+
middleware(mockReq, mockRes, mockNext);
|
|
231
|
+
expect(mockUploader.array).toHaveBeenCalledWith(expect.any(String), 30);
|
|
232
|
+
expect(mockNext).toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
it("should pass multer errors to next", () => {
|
|
235
|
+
const multerError = new multer_1.default.MulterError("LIMIT_FILE_COUNT", "fieldname");
|
|
236
|
+
mockUploader.array.mockReturnValueOnce((req, res, next) => next(multerError));
|
|
237
|
+
const middleware = fileUploaderService.handleMultipleUpload();
|
|
238
|
+
middleware(mockReq, mockRes, mockNext);
|
|
239
|
+
expect(mockNext).toHaveBeenCalledWith(multerError);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe("handleDeleteSingleFile", () => {
|
|
243
|
+
it("should delete the file if it exists", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
244
|
+
const middleware = fileUploaderService.handleDeleteSingleFile("path/to/file.jpg");
|
|
245
|
+
yield middleware(mockReq, mockRes, mockNext);
|
|
246
|
+
expect(mockStat).toHaveBeenCalledWith("path/to/file.jpg");
|
|
247
|
+
expect(mockUnlink).toHaveBeenCalledWith("path/to/file.jpg");
|
|
248
|
+
expect(mockNext).toHaveBeenCalled();
|
|
249
|
+
}));
|
|
250
|
+
it("should call next even if file doesn't exist", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
251
|
+
const middleware = fileUploaderService.handleDeleteSingleFile("nonexistent/file.jpg");
|
|
252
|
+
yield middleware(mockReq, mockRes, mockNext);
|
|
253
|
+
expect(mockNext).toHaveBeenCalled();
|
|
254
|
+
}));
|
|
255
|
+
});
|
|
256
|
+
describe("deleteFileByUrl", () => {
|
|
257
|
+
it("should delete a file by its URL", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
258
|
+
const result = yield fileUploaderService.deleteFileByUrl("http://localhost:3000/api/uploads/images/file.jpg");
|
|
259
|
+
expect(mockStat).toHaveBeenCalled();
|
|
260
|
+
expect(mockUnlink).toHaveBeenCalled();
|
|
261
|
+
expect(result).toBe(true);
|
|
262
|
+
}));
|
|
263
|
+
it("should throw AppError if file URL is invalid", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
264
|
+
yield expect(fileUploaderService.deleteFileByUrl("http://localhost:3000/invalid/path")).rejects.toBeInstanceOf(app_error_1.default);
|
|
265
|
+
expect(app_error_1.default).toHaveBeenCalledWith("Invalid file URL: base route not found", 400);
|
|
266
|
+
}));
|
|
267
|
+
it("should throw AppError if file doesn't exist", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
268
|
+
const enoentError = new Error("File not found");
|
|
269
|
+
enoentError.code = "ENOENT";
|
|
270
|
+
mockStat.mockImplementationOnce((path, mode, cb) => {
|
|
271
|
+
throw enoentError;
|
|
272
|
+
});
|
|
273
|
+
yield expect(fileUploaderService.deleteFileByUrl("http://localhost:3000/api/uploads/images/nonexistent.jpg")).rejects.toBeInstanceOf(app_error_1.default);
|
|
274
|
+
expect(app_error_1.default).toHaveBeenCalledWith("File not found", 404);
|
|
275
|
+
}));
|
|
276
|
+
});
|
|
277
|
+
describe("upload", () => {
|
|
278
|
+
beforeEach(() => {
|
|
279
|
+
mockReq.query = { multiple: "false" };
|
|
280
|
+
mockReq.file = {
|
|
281
|
+
path: "test.jpg",
|
|
282
|
+
originalname: "test.jpg",
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
it("should handle single file upload successfully", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
286
|
+
mockUploader.single.mockReturnValueOnce((req, res, next) => {
|
|
287
|
+
req.file = mockReq.file;
|
|
288
|
+
next();
|
|
289
|
+
});
|
|
290
|
+
const result = yield fileUploaderService.upload(mockReq, mockRes);
|
|
291
|
+
expect(result).toBe("http://localhost:3000/api/uploads/images/test.jpg");
|
|
292
|
+
}));
|
|
293
|
+
it("should handle multiple file uploads successfully", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
294
|
+
mockReq.query = { multiple: "true" };
|
|
295
|
+
mockReq.files = [
|
|
296
|
+
{ path: "test1.jpg", originalname: "test1.jpg" },
|
|
297
|
+
{ path: "test2.jpg", originalname: "test2.jpg" },
|
|
298
|
+
];
|
|
299
|
+
mockReq.file = null;
|
|
300
|
+
mockUploader.array.mockReturnValueOnce((req, res, next) => {
|
|
301
|
+
req.files = mockReq.files;
|
|
302
|
+
next();
|
|
303
|
+
});
|
|
304
|
+
const result = yield fileUploaderService.upload(mockReq, mockRes);
|
|
305
|
+
expect(Array.isArray(result)).toBe(true);
|
|
306
|
+
expect(result).toHaveLength(2);
|
|
307
|
+
expect(result[0]).toBe("http://localhost:3000/api/uploads/images/test1.jpg");
|
|
308
|
+
expect(result[1]).toBe("http://localhost:3000/api/uploads/images/test2.jpg");
|
|
309
|
+
}));
|
|
310
|
+
it("should process images with Sharp when uploading image files", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
311
|
+
mockReq.file = {
|
|
312
|
+
path: "test.jpg",
|
|
313
|
+
originalname: "test.jpg",
|
|
314
|
+
};
|
|
315
|
+
fileUploaderService["uploadDir"] = "/uploads/images/";
|
|
316
|
+
const imageUploaderService = new file_uploader_service_1.FileUploaderService("uploads/images");
|
|
317
|
+
mockUploader.single.mockReturnValueOnce((req, res, next) => {
|
|
318
|
+
req.file = mockReq.file;
|
|
319
|
+
next();
|
|
320
|
+
});
|
|
321
|
+
const result = yield imageUploaderService.upload(mockReq, mockRes, {
|
|
322
|
+
width: 300,
|
|
323
|
+
height: 200,
|
|
324
|
+
format: "webp",
|
|
325
|
+
});
|
|
326
|
+
expect(sharp_1.default).toHaveBeenCalledWith("test.jpg");
|
|
327
|
+
expect(mockSharp.resize).toHaveBeenCalledWith(300, 200, expect.any(Object));
|
|
328
|
+
expect(mockSharp.toFormat).toHaveBeenCalledWith("webp");
|
|
329
|
+
expect(mockSharp.toFile).toHaveBeenCalled();
|
|
330
|
+
expect(mockRename).toHaveBeenCalled();
|
|
331
|
+
expect(result).toBe("http://localhost:3000/api/uploads/images/test.jpg");
|
|
332
|
+
}));
|
|
333
|
+
it("should throw AppError if no file is uploaded", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
334
|
+
mockReq.file = null;
|
|
335
|
+
mockReq.files = null;
|
|
336
|
+
mockUploader.single.mockReturnValueOnce((req, res, next) => next());
|
|
337
|
+
yield expect(fileUploaderService.upload(mockReq, mockRes)).rejects.toBeInstanceOf(app_error_1.default);
|
|
338
|
+
expect(app_error_1.default).toHaveBeenCalledWith("No file uploaded", 400);
|
|
339
|
+
}));
|
|
340
|
+
it("should handle upload errors", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
341
|
+
const uploadError = new Error("Upload failed");
|
|
342
|
+
mockUploader.single.mockReturnValueOnce((req, res, next) => next(uploadError));
|
|
343
|
+
yield expect(fileUploaderService.upload(mockReq, mockRes)).rejects.toEqual(uploadError);
|
|
344
|
+
}));
|
|
345
|
+
it("should handle image processing errors", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
346
|
+
mockReq.file = {
|
|
347
|
+
path: "uploads/images/test.jpg",
|
|
348
|
+
originalname: "test.jpg",
|
|
349
|
+
};
|
|
350
|
+
fileUploaderService["uploadDir"] = "./uploads/images/";
|
|
351
|
+
mockUploader.single.mockReturnValueOnce((req, res, next) => {
|
|
352
|
+
req.file = mockReq.file;
|
|
353
|
+
next();
|
|
354
|
+
});
|
|
355
|
+
const processingError = new Error("Image processing failed");
|
|
356
|
+
mockSharp.toFile.mockRejectedValueOnce(processingError);
|
|
357
|
+
yield expect(fileUploaderService.upload(mockReq, mockRes, { format: "webp" })).rejects.toEqual(processingError);
|
|
358
|
+
}));
|
|
359
|
+
});
|
|
360
|
+
describe("getFileUploaderServices", () => {
|
|
361
|
+
it("should return all file uploader services", () => {
|
|
362
|
+
const services = (0, file_uploader_service_1.getFileUploaderServices)();
|
|
363
|
+
expect(services).toHaveProperty("imageUploaderService");
|
|
364
|
+
expect(services).toHaveProperty("videoUploaderService");
|
|
365
|
+
expect(services).toHaveProperty("documentUploaderService");
|
|
366
|
+
expect(services).toHaveProperty("fileUploaderService");
|
|
367
|
+
expect(services.imageUploaderService).toBeInstanceOf(file_uploader_service_1.FileUploaderService);
|
|
368
|
+
expect(services.videoUploaderService).toBeInstanceOf(file_uploader_service_1.FileUploaderService);
|
|
369
|
+
expect(services.documentUploaderService).toBeInstanceOf(file_uploader_service_1.FileUploaderService);
|
|
370
|
+
expect(services.fileUploaderService).toBeInstanceOf(file_uploader_service_1.FileUploaderService);
|
|
371
|
+
});
|
|
372
|
+
it("should use config values when available", () => {
|
|
373
|
+
server_1.getArkosConfig.mockReturnValue({
|
|
374
|
+
fileUpload: {
|
|
375
|
+
baseRoute: "/custom/api/uploads",
|
|
376
|
+
baseUploadDir: "/custom/uploads",
|
|
377
|
+
restrictions: {
|
|
378
|
+
images: {
|
|
379
|
+
maxCount: 50,
|
|
380
|
+
maxSize: 20 * 1024 * 1024,
|
|
381
|
+
supportedFilesRegex: /custom-regex/,
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
const services = (0, file_uploader_service_1.getFileUploaderServices)();
|
|
387
|
+
expect(services.imageUploaderService["uploadDir"]).toBe("./custom/uploads/images/");
|
|
388
|
+
expect(services.imageUploaderService["maxCount"]).toBe(50);
|
|
389
|
+
expect(services.imageUploaderService["fileSizeLimit"]).toBe(20 * 1024 * 1024);
|
|
390
|
+
expect(services.imageUploaderService["allowedFileTypes"]).toEqual(/custom-regex/);
|
|
391
|
+
});
|
|
392
|
+
it("should use default values when config is not available", () => {
|
|
393
|
+
server_1.getArkosConfig.mockReturnValue({});
|
|
394
|
+
const services = (0, file_uploader_service_1.getFileUploaderServices)();
|
|
395
|
+
expect(services.imageUploaderService["uploadDir"]).toBe("./uploads/images/");
|
|
396
|
+
expect(services.videoUploaderService["uploadDir"]).toBe("./uploads/videos/");
|
|
397
|
+
expect(services.documentUploaderService["uploadDir"]).toBe("./uploads/documents/");
|
|
398
|
+
expect(services.fileUploaderService["uploadDir"]).toBe("./uploads/files/");
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
//# sourceMappingURL=file-uploader.service.test.js.map
|