create-express-mongo-ts 1.0.0

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.
Files changed (54) hide show
  1. package/README.md +157 -0
  2. package/bin/cli.js +217 -0
  3. package/package.json +43 -0
  4. package/template/.dockerignore +2 -0
  5. package/template/.prettierignore +6 -0
  6. package/template/.prettierrc +8 -0
  7. package/template/Dockerfile +17 -0
  8. package/template/README.md +67 -0
  9. package/template/eslint.config.mts +34 -0
  10. package/template/jest.config.ts +201 -0
  11. package/template/keys/README.md +2 -0
  12. package/template/nodemon.json +5 -0
  13. package/template/package.json +65 -0
  14. package/template/src/app.ts +42 -0
  15. package/template/src/config.ts +31 -0
  16. package/template/src/core/ApiError.ts +118 -0
  17. package/template/src/core/ApiResponse.ts +140 -0
  18. package/template/src/core/asyncHandler.ts +15 -0
  19. package/template/src/core/authUtils.ts +68 -0
  20. package/template/src/core/jwtUtils.ts +96 -0
  21. package/template/src/core/logger.ts +48 -0
  22. package/template/src/core/utils.ts +12 -0
  23. package/template/src/database/index.ts +56 -0
  24. package/template/src/database/models/ApiKeys.ts +62 -0
  25. package/template/src/database/models/Keystore.ts +45 -0
  26. package/template/src/database/models/Role.ts +27 -0
  27. package/template/src/database/models/User.ts +64 -0
  28. package/template/src/database/repositories/ApiKeyRepo.ts +26 -0
  29. package/template/src/database/repositories/KeystoreRepo.ts +53 -0
  30. package/template/src/database/repositories/UserRepo.ts +63 -0
  31. package/template/src/helpers/generateApiKey.ts +23 -0
  32. package/template/src/helpers/validator.ts +38 -0
  33. package/template/src/index.ts +36 -0
  34. package/template/src/middlewares/authorize.middleware.ts +26 -0
  35. package/template/src/middlewares/error.middleware.ts +42 -0
  36. package/template/src/middlewares/permission.middleware.ts +20 -0
  37. package/template/src/middlewares/validator.middleware.ts +170 -0
  38. package/template/src/routes/auth/apiKey.ts +29 -0
  39. package/template/src/routes/auth/authentication.ts +45 -0
  40. package/template/src/routes/auth/index.ts +14 -0
  41. package/template/src/routes/auth/schema.ts +34 -0
  42. package/template/src/routes/auth/signin.ts +47 -0
  43. package/template/src/routes/auth/signout.ts +20 -0
  44. package/template/src/routes/auth/signup.ts +49 -0
  45. package/template/src/routes/auth/token.ts +68 -0
  46. package/template/src/routes/health/index.ts +14 -0
  47. package/template/src/routes/index.ts +19 -0
  48. package/template/src/types/ApiKey.ts +13 -0
  49. package/template/src/types/Keystore.ts +12 -0
  50. package/template/src/types/Role.ts +14 -0
  51. package/template/src/types/User.ts +16 -0
  52. package/template/src/types/app-requests.d.ts +22 -0
  53. package/template/src/types/permissions.ts +3 -0
  54. package/template/tsconfig.json +33 -0
@@ -0,0 +1,96 @@
1
+ import path from 'path';
2
+ import { readFile } from 'fs';
3
+ import { promisify } from 'util';
4
+ import { sign, verify, TokenExpiredError as JwtTokenExpiredError } from 'jsonwebtoken';
5
+ import { InternalError, BadTokenError, TokenExpiredError } from './ApiError';
6
+
7
+ /*
8
+ * issuer — Software organization who issues the token.
9
+ * subject — Intended user of the token.
10
+ * audience — Basically identity of the intended recipient of the token.
11
+ * expiresIn — Expiration time after which the token will be invalid.
12
+ * algorithm — Encryption algorithm to be used to protect the token.
13
+ */
14
+
15
+ export class JwtPayload {
16
+ aud: string;
17
+ sub: string;
18
+ iss: string;
19
+ iat: number;
20
+ exp: number;
21
+ prm: string;
22
+
23
+ constructor(
24
+ issuer: string,
25
+ audience: string,
26
+ subject: string,
27
+ param: string,
28
+ validity: number,
29
+ ) {
30
+ this.iss = issuer;
31
+ this.aud = audience;
32
+ this.sub = subject;
33
+ this.iat = Math.floor(Date.now() / 1000);
34
+ this.exp = this.iat + validity;
35
+ this.prm = param;
36
+ }
37
+ }
38
+
39
+ async function readPublicKey(): Promise<string> {
40
+ return promisify(readFile)(
41
+ path.join(__dirname, '../../keys/public.pem'),
42
+ 'utf8',
43
+ );
44
+ }
45
+
46
+ async function readPrivateKey(): Promise<string> {
47
+ return promisify(readFile)(
48
+ path.join(__dirname, '../../keys/private.pem'),
49
+ 'utf8',
50
+ );
51
+ }
52
+
53
+ async function encode(payload: JwtPayload): Promise<string> {
54
+ const cert = await readPrivateKey();
55
+ if (!cert) throw new InternalError('Token generation failure');
56
+ // @ts-expect-error cert is valid
57
+ return promisify(sign)({ ...payload }, cert, { algorithm: 'RS256' });
58
+ }
59
+
60
+ /**
61
+ * This method checks the token and returns the decoded data when token is valid in all respect
62
+ */
63
+ async function validate(token: string): Promise<JwtPayload> {
64
+ const cert = await readPublicKey();
65
+ try {
66
+ // @ts-expect-error cert is valid
67
+ return (await promisify(verify)(token, cert)) as JwtPayload;
68
+ } catch (e) {
69
+ if (e instanceof JwtTokenExpiredError) {
70
+ throw new TokenExpiredError();
71
+ }
72
+ // throws error if the token has not been encrypted by the private key
73
+ throw new BadTokenError();
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Returns the decoded payload if the signature is valid even if it is expired
79
+ */
80
+ async function decode(token: string): Promise<JwtPayload> {
81
+ const cert = await readPublicKey();
82
+ try {
83
+ // @ts-expect-error cert is valid
84
+ return (await promisify(verify)(token, cert, {
85
+ ignoreExpiration: true,
86
+ })) as JwtPayload;
87
+ } catch (_e) {
88
+ throw new BadTokenError();
89
+ }
90
+ }
91
+
92
+ export default {
93
+ encode,
94
+ validate,
95
+ decode,
96
+ };
@@ -0,0 +1,48 @@
1
+ import { createLogger, transports, format } from 'winston';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import DailyRotateFile from 'winston-daily-rotate-file';
5
+ import { isProduction, logDirectory } from '../config.js';
6
+
7
+ let dir = logDirectory;
8
+ if (!dir) {
9
+ dir = path.resolve('logs');
10
+ }
11
+
12
+ // create directory if it is not present
13
+ if (!fs.existsSync(dir)) {
14
+ // Create the directory if it does not exist
15
+ fs.mkdirSync(dir);
16
+ }
17
+
18
+ const logLevel = (!isProduction ? 'debug' : 'warn');
19
+
20
+ const dailyRotateFile = new DailyRotateFile({
21
+ level: logLevel,
22
+ filename: dir + '/%DATE%.log',
23
+ datePattern: 'YYYY-MM-DD',
24
+ zippedArchive: true,
25
+ handleExceptions: true,
26
+ maxSize: '20m',
27
+ maxFiles: '14d',
28
+ format: format.combine(
29
+ format.errors({ stack: true }),
30
+ format.timestamp(),
31
+ format.json(),
32
+ ),
33
+ });
34
+
35
+ export default createLogger({
36
+ transports: [
37
+ new transports.Console({
38
+ level: logLevel,
39
+ format: format.combine(
40
+ format.errors({ stack: true }),
41
+ format.prettyPrint(),
42
+ ),
43
+ }),
44
+ dailyRotateFile,
45
+ ],
46
+ exceptionHandlers: [dailyRotateFile],
47
+ exitOnError: false, // do not exit on handled exceptions
48
+ });
@@ -0,0 +1,12 @@
1
+ import User from "./../types/User";
2
+ import objectManipulator from "lodash";
3
+
4
+ export const enum Header {
5
+ API_KEY = 'x-api-key',
6
+ AUTHORIZATION = 'authorization',
7
+ }
8
+
9
+ export async function getUserData(user: User) {
10
+ const data = objectManipulator.pick(user, ['_id', 'name', 'roles', 'profilePicUrl', 'email']);
11
+ return data;
12
+ }
@@ -0,0 +1,56 @@
1
+ import mongoose, { Query, Schema } from 'mongoose';
2
+ import logger from '../core/logger';
3
+ import { db } from '../config';
4
+
5
+ export async function connectDB() {
6
+ const dbURI = `mongodb+srv://${db.user}:${encodeURIComponent(db.password)}@${
7
+ db.host
8
+ }/${db.name}`;
9
+
10
+ const options = {
11
+ autoIndex: true,
12
+ minPoolSize: db.minPoolSize,
13
+ maxPoolSize: db.maxPoolSize,
14
+ connectTimeoutMS: 60000,
15
+ socketTimeoutMS: 45000,
16
+ };
17
+ mongoose.set('strictQuery', true);
18
+
19
+ function setRunValidators(this: Query<unknown, unknown>) {
20
+ this.setOptions({ runValidators: true });
21
+ }
22
+
23
+ mongoose.plugin((schema: Schema) => {
24
+ schema.pre('findOneAndUpdate', setRunValidators);
25
+ schema.pre('updateMany', setRunValidators);
26
+ schema.pre('updateOne', setRunValidators);
27
+ schema.pre(/^update/, setRunValidators);
28
+ });
29
+ try {
30
+ await mongoose.connect(dbURI, options);
31
+ logger.info('Mongoose connection established');
32
+ } catch (err) {
33
+ logger.error('Mongoose connection error');
34
+ logger.error(err);
35
+ process.exit(1);
36
+ }
37
+
38
+ mongoose.connection.on('connected', () => {
39
+ logger.debug(`Mongoose connected to ${dbURI}`);
40
+ });
41
+
42
+ mongoose.connection.on('error', (err) => {
43
+ logger.error('Mongoose connection error: ' + err);
44
+ });
45
+
46
+ mongoose.connection.on('disconnected', () => {
47
+ logger.info('Mongoose disconnected');
48
+ });
49
+
50
+ process.on('SIGINT', () => {
51
+ mongoose.connection.close().finally(() => {
52
+ logger.info('Mongoose disconnected due to app termination');
53
+ process.exit(0);
54
+ });
55
+ });
56
+ }
@@ -0,0 +1,62 @@
1
+ import { Schema, model } from 'mongoose';
2
+ import { Permission } from '../../types/permissions.js';
3
+ import ApiKey from '../../types/ApiKey.js';
4
+
5
+ export const DOCUMENT_NAME = 'ApiKey';
6
+ export const COLLECTION_NAME = 'api_keys';
7
+
8
+
9
+ const schema = new Schema<ApiKey>(
10
+ {
11
+ key: {
12
+ type: Schema.Types.String,
13
+ required: true,
14
+ unique: true,
15
+ maxlength: 1024,
16
+ trim: true,
17
+ },
18
+ version: {
19
+ type: Schema.Types.Number,
20
+ required: true,
21
+ min: 1,
22
+ max: 100,
23
+ },
24
+ permissions: {
25
+ type: [
26
+ {
27
+ type: Schema.Types.String,
28
+ required: true,
29
+ enum: Object.values(Permission),
30
+ },
31
+ ],
32
+ required: true,
33
+ },
34
+ comments: {
35
+ type: [
36
+ {
37
+ type: Schema.Types.String,
38
+ required: true,
39
+ trim: true,
40
+ maxlength: 1000,
41
+ },
42
+ ],
43
+ required: true,
44
+ },
45
+ status: {
46
+ type: Schema.Types.Boolean,
47
+ default: true,
48
+ },
49
+ },
50
+ {
51
+ versionKey: false,
52
+ timestamps: true
53
+ }
54
+ );
55
+
56
+ schema.index({ key: 1, status: 1 });
57
+
58
+ export const ApiKeyModel = model<ApiKey>(
59
+ DOCUMENT_NAME,
60
+ schema,
61
+ COLLECTION_NAME,
62
+ );
@@ -0,0 +1,45 @@
1
+ import { Schema, model } from 'mongoose';
2
+ import Keystore from '../../types/Keystore';
3
+
4
+ export const DOCUMENT_NAME = 'Keystore';
5
+ export const COLLECTION_NAME = 'keystores';
6
+
7
+
8
+
9
+ const schema = new Schema<Keystore>(
10
+ {
11
+ client: {
12
+ type: Schema.Types.ObjectId,
13
+ required: true,
14
+ ref: 'User',
15
+ },
16
+ primaryKey: {
17
+ type: Schema.Types.String,
18
+ required: true,
19
+ trim: true,
20
+ },
21
+ secondaryKey: {
22
+ type: Schema.Types.String,
23
+ required: true,
24
+ trim: true,
25
+ },
26
+ status: {
27
+ type: Schema.Types.Boolean,
28
+ default: true,
29
+ },
30
+ },
31
+ {
32
+ versionKey: false,
33
+ timestamps: true
34
+ },
35
+ );
36
+
37
+ schema.index({ client: 1 });
38
+ schema.index({ client: 1, primaryKey: 1, status: 1 });
39
+ schema.index({ client: 1, primaryKey: 1, secondaryKey: 1 });
40
+
41
+ export const KeystoreModel = model<Keystore>(
42
+ DOCUMENT_NAME,
43
+ schema,
44
+ COLLECTION_NAME,
45
+ );
@@ -0,0 +1,27 @@
1
+ import { Schema, model } from 'mongoose';
2
+ import Role, { RoleCode } from './../../types/Role.js';
3
+
4
+ export const DOCUMENT_NAME = 'Role';
5
+ export const COLLECTION_NAME = 'roles';
6
+
7
+ const schema = new Schema<Role>(
8
+ {
9
+ code: {
10
+ type: Schema.Types.String,
11
+ required: true,
12
+ enum: Object.values(RoleCode),
13
+ },
14
+ status: {
15
+ type: Schema.Types.Boolean,
16
+ default: true,
17
+ },
18
+ },
19
+ {
20
+ timestamps: true,
21
+ versionKey: false,
22
+ },
23
+ );
24
+
25
+ schema.index({ code: 1, status: 1 });
26
+
27
+ export const RoleModel = model<Role>(DOCUMENT_NAME, schema, COLLECTION_NAME);
@@ -0,0 +1,64 @@
1
+ import { model, Schema } from "mongoose";
2
+ import bcryptjs from "bcryptjs";
3
+ import User from "../../types/User";
4
+
5
+
6
+ export const DOCUMENT_NAME = 'User';
7
+ export const COLLECTION_NAME = 'users';
8
+
9
+
10
+ const schema = new Schema<User>({
11
+ name: {
12
+ type: Schema.Types.String,
13
+ trim: true,
14
+ maxLength: 200,
15
+ },
16
+ email: {
17
+ type: Schema.Types.String,
18
+ unique: true,
19
+ trim: true,
20
+ toLowerCase: true,
21
+ select: false
22
+ },
23
+ password: {
24
+ type: Schema.Types.String,
25
+ select: false,
26
+ },
27
+ roles: {
28
+ type: [
29
+ {
30
+ type: Schema.Types.ObjectId,
31
+ ref: 'Role',
32
+ },
33
+ ],
34
+ required: true,
35
+ select: false,
36
+ },
37
+ verified: {
38
+ type: Schema.Types.Boolean,
39
+ default: false,
40
+ },
41
+ status: {
42
+ type: Schema.Types.Boolean,
43
+ default: true,
44
+ }
45
+ }, {
46
+ timestamps: true,
47
+ versionKey: false
48
+ });
49
+
50
+ schema.index({ _id: 1, status: 1 });
51
+ schema.index({ status: 1 });
52
+
53
+ // Pre hook to save the password as hashed password
54
+ schema.pre("save", async function (next) {
55
+ if (!this.isModified("password")) {
56
+ return next();
57
+ }
58
+ this.password = await bcryptjs.hash(this.password, 12);
59
+ next();
60
+ });
61
+
62
+
63
+
64
+ export const UserModel = model<User>(DOCUMENT_NAME, schema, COLLECTION_NAME);
@@ -0,0 +1,26 @@
1
+ import { Permission } from '../../types/permissions';
2
+ import ApiKey from '../../types/ApiKey';
3
+ import { ApiKeyModel } from '../models/ApiKeys';
4
+
5
+ async function findByKey(key: string): Promise<ApiKey | null> {
6
+ return ApiKeyModel.findOne({ key: key, status: true }).lean().exec();
7
+ }
8
+
9
+ async function create(
10
+ key: string,
11
+ comments: string[],
12
+ permissions: Permission[],
13
+ version: number = 1,
14
+ ) {
15
+ return ApiKeyModel.create({
16
+ key,
17
+ comments,
18
+ permissions,
19
+ version,
20
+ });
21
+ }
22
+
23
+ export default {
24
+ findByKey,
25
+ create
26
+ };
@@ -0,0 +1,53 @@
1
+ import Keystore from '../../types/Keystore';
2
+ import User from '../../types/User';
3
+ import { KeystoreModel } from '../models/Keystore';
4
+ import { Types } from 'mongoose';
5
+
6
+ async function create(
7
+ client: User,
8
+ primaryKey: string,
9
+ secondaryKey: string,
10
+ ): Promise<Keystore> {
11
+ const keystore = await KeystoreModel.create({
12
+ client: client,
13
+ primaryKey: primaryKey,
14
+ secondaryKey: secondaryKey,
15
+ });
16
+
17
+ return keystore.toObject();
18
+ }
19
+
20
+ async function remove(id: Types.ObjectId) {
21
+ return KeystoreModel.findByIdAndDelete(id).lean().exec();
22
+ }
23
+
24
+ async function findForKey(client: User, key: string) {
25
+ return KeystoreModel.findOne({
26
+ client: client,
27
+ primaryKey: key,
28
+ status: true,
29
+ })
30
+ .lean()
31
+ .exec();
32
+ }
33
+
34
+ async function find(
35
+ client: User,
36
+ primaryKey: string,
37
+ secondaryKey: string,
38
+ ): Promise<Keystore | null> {
39
+ return KeystoreModel.findOne({
40
+ client: client,
41
+ primaryKey: primaryKey,
42
+ secondaryKey: secondaryKey,
43
+ })
44
+ .lean()
45
+ .exec();
46
+ }
47
+
48
+ export default {
49
+ create,
50
+ remove,
51
+ findForKey,
52
+ find
53
+ };
@@ -0,0 +1,63 @@
1
+ import User from "../../types/User";
2
+ import { InternalError } from "../../core/ApiError";
3
+ import { RoleModel } from "../models/Role";
4
+ import { UserModel } from "../models/User";
5
+ import KeystoreRepo from "./KeystoreRepo";
6
+ import { Types } from "mongoose";
7
+
8
+ async function findByEmail(email: string) {
9
+ return await UserModel.findOne({ email: email })
10
+ .select(
11
+ "+name +email +password +roles"
12
+ )
13
+ .populate({
14
+ path: "roles",
15
+ match: { status: true },
16
+ select: { code: 1 }
17
+ })
18
+ .lean()
19
+ .exec();
20
+ }
21
+
22
+ async function create(
23
+ user: User,
24
+ accessTokenKey: string,
25
+ refreshTokenKey: string,
26
+ roleCode: string
27
+ ) {
28
+ const role = await RoleModel.findOne({ code: roleCode })
29
+ .select("+code")
30
+ .lean()
31
+ .exec();
32
+
33
+ if (!role) throw new InternalError("Role must be defined.");
34
+
35
+ user.roles = [role];
36
+ const userCreated = await UserModel.create(user);
37
+
38
+ const keystore = await KeystoreRepo.create(
39
+ userCreated, accessTokenKey, refreshTokenKey
40
+ );
41
+ return {
42
+ user: { ...userCreated.toObject(), roles: user.roles },
43
+ keystore: keystore
44
+ };
45
+ }
46
+
47
+
48
+ async function findById(id: Types.ObjectId) {
49
+ return await UserModel.findOne({ _id: id, status: true })
50
+ .select("+email +password +name +roles")
51
+ .populate({
52
+ path: "roles",
53
+ match: { status: true }
54
+ })
55
+ .lean()
56
+ .exec();
57
+ }
58
+
59
+ export default {
60
+ findByEmail,
61
+ create,
62
+ findById
63
+ };
@@ -0,0 +1,23 @@
1
+ import crypto from 'crypto';
2
+ import ApiKeyRepo from '../database/repositories/ApiKeyRepo';
3
+ import { Permission } from '../types/permissions';
4
+ import { connectDB } from '../database';
5
+
6
+ export async function createApiKey(
7
+ comments: string[],
8
+ permissions: Permission[],
9
+ ) {
10
+ const key = crypto.randomBytes(32).toString('hex');
11
+
12
+ const newKey = await ApiKeyRepo.create(key, comments, permissions, 1);
13
+
14
+ if (!newKey) {
15
+ throw Error('Failed to generate API Key.');
16
+ }
17
+
18
+ console.log('Your API key:', key);
19
+ return key;
20
+ }
21
+ connectDB().then(async () => {
22
+ await createApiKey(['API Key for testing.'], [Permission.GENERAL]);
23
+ })
@@ -0,0 +1,38 @@
1
+ import mongoose from 'mongoose';
2
+ import z from 'zod';
3
+
4
+ export enum ValidationSource {
5
+ BODY = 'body',
6
+ HEADER = 'headers',
7
+ QUERY = 'query',
8
+ PARAM = 'params',
9
+ }
10
+
11
+ export const ZodObjectId = z
12
+ .string()
13
+ .refine((value: string) => mongoose.Types.ObjectId.isValid(value), {
14
+ message: 'Invalid mongoDB object Id.',
15
+ });
16
+
17
+ export const ZodUrlEndpoint = z
18
+ .string()
19
+ .refine((value: string) => !value.includes('://'), {
20
+ message: 'Invalid endpoint: URLs with protocol are not allowed',
21
+ });
22
+
23
+ export const ZodAuthBearer = z.string().refine(
24
+ (value: string) => {
25
+ if (!value.startsWith('Bearer ')) return false;
26
+
27
+ const parts = value.split(' ');
28
+ if (parts.length !== 2) return false;
29
+
30
+ const token = parts[1];
31
+ if (!token || token.trim().length === 0) return false;
32
+
33
+ return true;
34
+ },
35
+ {
36
+ message: "Invalid Authorization header. Expected: 'Bearer <token>'",
37
+ },
38
+ );
@@ -0,0 +1,36 @@
1
+ process.on('uncaughtException', (err) => {
2
+ console.error('Uncaught Exception:', err);
3
+ process.exit(1);
4
+ });
5
+
6
+ process.on('unhandledRejection', (reason, promise) => {
7
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
8
+ process.exit(1);
9
+ });
10
+
11
+ import logger from './core/logger.js';
12
+ import { port } from './config.js';
13
+ import { connectDB } from './database/index.js';
14
+ import { app } from './app.js';
15
+
16
+ async function start() {
17
+ try {
18
+ await connectDB(); // Connect DB first
19
+ logger.info('Database connected');
20
+ logger.info('App loaded');
21
+
22
+ app.listen(port, () => {
23
+ logger.info(`Server running on port: ${port}`);
24
+ });
25
+ } catch (err) {
26
+ logger.error('Startup error');
27
+ logger.error(err);
28
+ process.exit(1);
29
+ }
30
+ }
31
+
32
+ start()
33
+ .catch((err) => {
34
+ logger.error('Fatal startup error.', err);
35
+ process.exit(1);
36
+ })
@@ -0,0 +1,26 @@
1
+ import { RequestHandler } from 'express';
2
+ import { RoleCode } from '../types/Role';
3
+ import { ProtectedRequest } from '../types/app-requests';
4
+ import { ForbiddenError } from '../core/ApiError';
5
+
6
+ export const authorize = (...allowedRoles: RoleCode[]): RequestHandler => {
7
+ return (req, _res, next) => {
8
+ try {
9
+ const protectedReq = req as ProtectedRequest;
10
+
11
+ if (!protectedReq.user) {
12
+ throw new ForbiddenError('Authentication required');
13
+ }
14
+
15
+ if (!allowedRoles.includes(protectedReq.user.role)) {
16
+ throw new ForbiddenError(
17
+ `Forbidden: required roles [${allowedRoles.join(', ')}], found: ${protectedReq.user.role}`,
18
+ );
19
+ }
20
+
21
+ next();
22
+ } catch (error) {
23
+ next(error);
24
+ }
25
+ };
26
+ };