@starklabs/backend-core 1.1.0 → 1.2.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.
- package/.env.example +21 -0
- package/README.md +7 -7
- package/dist/js/config/cloudinary.js +18 -0
- package/dist/js/config/config.js +11 -0
- package/dist/js/config/duration.js +22 -0
- package/dist/js/core/app.js +122 -0
- package/dist/js/core/auth/OTP.js +115 -0
- package/dist/js/core/auth/auth.controller.js +63 -0
- package/dist/js/core/auth/auth.service.js +290 -0
- package/dist/js/core/auth/auth.validation.js +95 -0
- package/dist/js/core/crud/crud.controller.js +95 -0
- package/dist/js/core/crud/crud.service.js +296 -0
- package/dist/js/core/index.js +3 -0
- package/dist/js/index.js +44 -55
- package/dist/js/lib/db.js +40 -0
- package/dist/js/lib/field.types.js +174 -0
- package/dist/js/lib/model.factory.js +19 -0
- package/dist/js/lib/model.registry.js +4 -0
- package/dist/js/lib/schema.builder.js +35 -0
- package/dist/js/lib/zod.validations.js +247 -0
- package/dist/js/middleware/auth.middleware.js +51 -0
- package/dist/js/middleware/error.middleware.js +28 -0
- package/dist/js/middleware/socket.middleware.js +29 -0
- package/dist/js/utils/AppLog.js +2 -1
- package/dist/js/utils/deleteFile.js +22 -0
- package/dist/js/utils/index.js +10 -1
- package/dist/js/utils/jwt.js +12 -20
- package/dist/js/utils/libsodium.js +19 -3
- package/dist/js/utils/rateLimiter.js +25 -0
- package/dist/js/utils/uploadFile.js +43 -0
- package/handlerMap.js +33 -0
- package/package.json +18 -5
- package/test.js +36 -0
- package/dist/cjs/db.cjs +0 -17
- package/dist/cjs/index.cjs +0 -59
- package/dist/cjs/utils/AppError.cjs +0 -13
- package/dist/cjs/utils/AppLog.cjs +0 -13
- package/dist/cjs/utils/asyncHandler.cjs +0 -6
- package/dist/cjs/utils/jwt.cjs +0 -38
- package/dist/cjs/utils/libsodium.cjs +0 -145
- package/dist/cjs/utils/successResponse.cjs +0 -13
- package/dist/js/db.js +0 -19
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import registerModel from "../../lib/model.registry.js";
|
|
2
|
+
import {
|
|
3
|
+
AppError,
|
|
4
|
+
hash,
|
|
5
|
+
verifyHash,
|
|
6
|
+
signJWT,
|
|
7
|
+
AppLog,
|
|
8
|
+
} from "../../utils/index.js";
|
|
9
|
+
import { getConfig } from "../../config/config.js";
|
|
10
|
+
|
|
11
|
+
// custom imports
|
|
12
|
+
import sendOTP from "./OTP.js";
|
|
13
|
+
|
|
14
|
+
// signup
|
|
15
|
+
const signup = async ({ body, Model, OTPModel }) => {
|
|
16
|
+
const foundUser = await Model.findOne({ email: body.email });
|
|
17
|
+
if (foundUser) {
|
|
18
|
+
if (foundUser.provider === "local")
|
|
19
|
+
throw new AppError(
|
|
20
|
+
"User with this email already exists. Please login.",
|
|
21
|
+
409,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (foundUser.provider === "google")
|
|
25
|
+
throw new AppError(
|
|
26
|
+
"Please login with Google. User with this email already exists with Google provider.",
|
|
27
|
+
409,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const reqOTPUser = await OTPModel.findOne({ email: body.email });
|
|
32
|
+
if (reqOTPUser) throw new AppError("You already requested an OTP.");
|
|
33
|
+
|
|
34
|
+
const { OTP, otpExpiry } = await sendOTP(body.email);
|
|
35
|
+
if (getConfig().isOffline) {
|
|
36
|
+
console.log("OTP:", OTP);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const hashedPassword = await hash(body.password);
|
|
40
|
+
const hashedOTP = await hash(OTP);
|
|
41
|
+
|
|
42
|
+
body.password = hashedPassword;
|
|
43
|
+
body.role = "user";
|
|
44
|
+
body = { ...body, otp: hashedOTP, otpExpiry, provider: "local" };
|
|
45
|
+
|
|
46
|
+
await OTPModel.create(body);
|
|
47
|
+
|
|
48
|
+
return { msg: "OTP sent successfully" };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// twoFactorAuth
|
|
52
|
+
const twoFactorAuth = async ({ body, Model, OTPModel }) => {
|
|
53
|
+
const { email, otp } = body;
|
|
54
|
+
const otpuser = await OTPModel.findOneAndUpdate(
|
|
55
|
+
{ email },
|
|
56
|
+
{ $inc: { otpCount: 1 } },
|
|
57
|
+
{ returnDocument: "after" },
|
|
58
|
+
)
|
|
59
|
+
.select("+password")
|
|
60
|
+
.lean();
|
|
61
|
+
|
|
62
|
+
if (!otpuser)
|
|
63
|
+
throw new AppError(
|
|
64
|
+
"You didn't request OTP. Please try re-login, signup or check your email.",
|
|
65
|
+
409,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// ifExpire
|
|
69
|
+
if (Date.now() > otpuser.otpExpiry) {
|
|
70
|
+
throw new AppError("OTP expired. Please request another one.", 409);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// anti brute force
|
|
74
|
+
if (otpuser.otpCount >= 10)
|
|
75
|
+
throw new AppError(
|
|
76
|
+
"OTP verification limit reached. Please request another one.",
|
|
77
|
+
409,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// ifInValid
|
|
81
|
+
const isValid = await verifyHash(otp.toString(), otpuser.otp);
|
|
82
|
+
if (!isValid) throw new AppError("Invalid OTP", 409);
|
|
83
|
+
|
|
84
|
+
if (otpuser.type === "forgotPassword") {
|
|
85
|
+
await OTPModel.updateOne({ email }, { $set: { status: "verified" } });
|
|
86
|
+
|
|
87
|
+
return { msg: "Verified successfully" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// insert in user
|
|
91
|
+
delete otpuser.otpExpiry;
|
|
92
|
+
delete otpuser.otp;
|
|
93
|
+
delete otpuser.otpCount;
|
|
94
|
+
|
|
95
|
+
const foundUser = await Model.findOne({ email });
|
|
96
|
+
if (!foundUser) {
|
|
97
|
+
await Model.create(otpuser);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
[
|
|
101
|
+
"password",
|
|
102
|
+
"createdAt",
|
|
103
|
+
"updatedAt",
|
|
104
|
+
"__v",
|
|
105
|
+
"provider",
|
|
106
|
+
"type",
|
|
107
|
+
"status",
|
|
108
|
+
].forEach((el) => {
|
|
109
|
+
delete otpuser[el];
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const { jwtSecret, tokenExpiry, isOffline } = getConfig();
|
|
113
|
+
if (!jwtSecret) throw new AppError("jwtSecret is missing in StarkCore({})");
|
|
114
|
+
if (!tokenExpiry)
|
|
115
|
+
throw new AppError("jwtExpiry is missing in StarkCore.create({})");
|
|
116
|
+
|
|
117
|
+
const token = signJWT(
|
|
118
|
+
{
|
|
119
|
+
email: otpuser.email,
|
|
120
|
+
id: otpuser._id,
|
|
121
|
+
role: otpuser.role,
|
|
122
|
+
},
|
|
123
|
+
jwtSecret,
|
|
124
|
+
tokenExpiry,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
await OTPModel.deleteOne({ email });
|
|
128
|
+
|
|
129
|
+
return { token, user: otpuser, msg: "Verified successfully" };
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// resendOTP
|
|
133
|
+
const resendOTP = async ({ body, OTPModel }) => {
|
|
134
|
+
const user = await OTPModel.findOne({ email: body.email });
|
|
135
|
+
if (!user)
|
|
136
|
+
throw new AppError(
|
|
137
|
+
"OTP can't be sent. Please try re-login or signup.",
|
|
138
|
+
409,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const { OTP, otpExpiry } = await sendOTP(body.email);
|
|
142
|
+
if (getConfig().isOffline) {
|
|
143
|
+
console.log("OTP: ", OTP);
|
|
144
|
+
}
|
|
145
|
+
const hashedOTP = await hash(OTP);
|
|
146
|
+
|
|
147
|
+
await OTPModel.updateOne(
|
|
148
|
+
{ email: body.email },
|
|
149
|
+
{
|
|
150
|
+
otp: hashedOTP,
|
|
151
|
+
otpExpiry,
|
|
152
|
+
otpCount: 0,
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return { msg: "OTP sent successfully" };
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// login
|
|
160
|
+
const login = async ({ body, Model, OTPModel }) => {
|
|
161
|
+
let user = await Model.findOne({ email: body.email })
|
|
162
|
+
.select("+password")
|
|
163
|
+
.lean();
|
|
164
|
+
|
|
165
|
+
const reqOTPUser = await OTPModel.findOne({ email: body.email });
|
|
166
|
+
if (reqOTPUser) throw new AppError("You already requested an OTP.");
|
|
167
|
+
|
|
168
|
+
if (!user)
|
|
169
|
+
throw new AppError(
|
|
170
|
+
"User with this email not found. Please create your account.",
|
|
171
|
+
404,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (user.provider === "google")
|
|
175
|
+
throw new AppError(
|
|
176
|
+
"Please login with Google. User with this email already exists with Google provider.",
|
|
177
|
+
409,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const isValid = await verifyHash(body.password, user.password);
|
|
181
|
+
if (!isValid) throw new AppError("Invalid password", 409);
|
|
182
|
+
|
|
183
|
+
const { OTP, otpExpiry } = await sendOTP(body.email);
|
|
184
|
+
if (getConfig().isOffline) {
|
|
185
|
+
console.log("OTP: ", OTP);
|
|
186
|
+
}
|
|
187
|
+
const hashedOTP = await hash(OTP);
|
|
188
|
+
|
|
189
|
+
user = { ...user, otp: hashedOTP, otpExpiry };
|
|
190
|
+
|
|
191
|
+
await OTPModel.create(user);
|
|
192
|
+
|
|
193
|
+
return { msg: "OTP sent successfully" };
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// forgotPassword
|
|
197
|
+
const forgotPassword = async ({ body, Model, OTPModel }) => {
|
|
198
|
+
let user = await Model.findOne({ email: body.email })
|
|
199
|
+
.select("+password")
|
|
200
|
+
.lean();
|
|
201
|
+
if (!user)
|
|
202
|
+
throw new AppError(
|
|
203
|
+
"User with this email not found. Please create your account.",
|
|
204
|
+
404,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const reqOTPUser = await OTPModel.findOne({ email: body.email });
|
|
208
|
+
if (reqOTPUser) {
|
|
209
|
+
if (reqOTPUser.type === "forgotPassword") {
|
|
210
|
+
throw new AppError("You already requested an OTP");
|
|
211
|
+
} else {
|
|
212
|
+
await OTPModel.deleteOne({ email: body.email });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const { OTP, otpExpiry } = await sendOTP(body.email);
|
|
217
|
+
if (getConfig().isOffline) {
|
|
218
|
+
console.log("OTP: ", OTP);
|
|
219
|
+
}
|
|
220
|
+
const hashedOTP = await hash(OTP);
|
|
221
|
+
|
|
222
|
+
await OTPModel.create({
|
|
223
|
+
...user,
|
|
224
|
+
otp: hashedOTP,
|
|
225
|
+
otpExpiry,
|
|
226
|
+
type: "forgotPassword",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return { msg: "OTP sent successfully" };
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// resetPassword
|
|
233
|
+
const resetPassword = async ({ body, Model, OTPModel }) => {
|
|
234
|
+
const { email, password } = body;
|
|
235
|
+
|
|
236
|
+
const otpUser = await OTPModel.findOne({ email: body.email });
|
|
237
|
+
if (!otpUser)
|
|
238
|
+
throw new AppError(
|
|
239
|
+
"You didn't requested OTP. Please send request to forgot-password.",
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (otpUser.type !== "forgotPassword")
|
|
243
|
+
throw new AppError("Unauthorized. Please request to forgot-password");
|
|
244
|
+
|
|
245
|
+
if (otpUser.status === "pending")
|
|
246
|
+
throw new AppError(
|
|
247
|
+
"You haven't verfied your email yet. Please use the OTP we just sent on your email",
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
let user = await Model.findOne({ email: body.email }).select("+password");
|
|
251
|
+
if (!user)
|
|
252
|
+
throw new AppError(
|
|
253
|
+
"User with this email not found. Please create your account.",
|
|
254
|
+
404,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
user.password = await hash(body.password);
|
|
258
|
+
|
|
259
|
+
await user.save();
|
|
260
|
+
|
|
261
|
+
await OTPModel.deleteOne({ email });
|
|
262
|
+
|
|
263
|
+
return { msg: "Password updated successfully" };
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const deleteAccount = async ({ body, Model }) => {
|
|
267
|
+
const { email, password } = body;
|
|
268
|
+
|
|
269
|
+
let user = await Model.findOne({ email }).select("+password").lean();
|
|
270
|
+
|
|
271
|
+
if (!user)
|
|
272
|
+
throw new AppError("User not found. Please check your email.", 404);
|
|
273
|
+
|
|
274
|
+
const isValid = await verifyHash(body.password, user.password);
|
|
275
|
+
if (!isValid) throw new AppError("Invalid password", 409);
|
|
276
|
+
|
|
277
|
+
await Model.deleteOne({ email });
|
|
278
|
+
|
|
279
|
+
return { msg: "Account deleted successfully" };
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export default {
|
|
283
|
+
login,
|
|
284
|
+
signup,
|
|
285
|
+
twoFactorAuth,
|
|
286
|
+
resendOTP,
|
|
287
|
+
forgotPassword,
|
|
288
|
+
resetPassword,
|
|
289
|
+
deleteAccount,
|
|
290
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { userIdSchema } from "../../utils/userIdValidation.js";
|
|
3
|
+
|
|
4
|
+
const emailSchema = z
|
|
5
|
+
.string()
|
|
6
|
+
.trim()
|
|
7
|
+
.min(1, "Email is required")
|
|
8
|
+
.email("Invalid email");
|
|
9
|
+
|
|
10
|
+
const passwordSchema = z
|
|
11
|
+
.string()
|
|
12
|
+
.min(12, "Password must be at least 12 characters");
|
|
13
|
+
|
|
14
|
+
const providerSchema = z.string().min(1, "Provider is required");
|
|
15
|
+
|
|
16
|
+
const nameSchema = (field) =>
|
|
17
|
+
z
|
|
18
|
+
.string()
|
|
19
|
+
.trim()
|
|
20
|
+
.min(1, `${field} is required`)
|
|
21
|
+
.min(3, `${field} must be at least 3 characters`)
|
|
22
|
+
.max(50, `${field} too long`);
|
|
23
|
+
|
|
24
|
+
// signup schema
|
|
25
|
+
const signupSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
firstName: nameSchema("FirstName"),
|
|
28
|
+
|
|
29
|
+
lastName: nameSchema("LastName"),
|
|
30
|
+
|
|
31
|
+
email: emailSchema,
|
|
32
|
+
|
|
33
|
+
password: passwordSchema,
|
|
34
|
+
|
|
35
|
+
role: z.string().default("user"),
|
|
36
|
+
})
|
|
37
|
+
.strict();
|
|
38
|
+
|
|
39
|
+
// 2fa schema
|
|
40
|
+
const twoFactorAuthSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
email: emailSchema,
|
|
43
|
+
|
|
44
|
+
otp: z.string().length(6, "OTP must be exactly 6 characters"),
|
|
45
|
+
})
|
|
46
|
+
.strict();
|
|
47
|
+
|
|
48
|
+
// resend OTP schema
|
|
49
|
+
const resendOTPSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
email: emailSchema,
|
|
52
|
+
})
|
|
53
|
+
.strict();
|
|
54
|
+
|
|
55
|
+
// login schema
|
|
56
|
+
const loginSchema = z
|
|
57
|
+
.object({
|
|
58
|
+
email: emailSchema,
|
|
59
|
+
password: passwordSchema,
|
|
60
|
+
})
|
|
61
|
+
.strict();
|
|
62
|
+
|
|
63
|
+
// forgot password schema
|
|
64
|
+
const forgotPasswordSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
email: emailSchema,
|
|
67
|
+
})
|
|
68
|
+
.strict();
|
|
69
|
+
|
|
70
|
+
// reset password schema
|
|
71
|
+
const resetPasswordSchema = z
|
|
72
|
+
.object({
|
|
73
|
+
email: emailSchema,
|
|
74
|
+
password: passwordSchema,
|
|
75
|
+
})
|
|
76
|
+
.strict();
|
|
77
|
+
|
|
78
|
+
// logout schema
|
|
79
|
+
const logoutSchema = z
|
|
80
|
+
.object({
|
|
81
|
+
email: emailSchema,
|
|
82
|
+
provider: providerSchema,
|
|
83
|
+
userId: userIdSchema,
|
|
84
|
+
})
|
|
85
|
+
.strict();
|
|
86
|
+
|
|
87
|
+
export default {
|
|
88
|
+
loginSchema,
|
|
89
|
+
signupSchema,
|
|
90
|
+
twoFactorAuthSchema,
|
|
91
|
+
resendOTPSchema,
|
|
92
|
+
forgotPasswordSchema,
|
|
93
|
+
resetPasswordSchema,
|
|
94
|
+
logoutSchema,
|
|
95
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { app } from "../app.js";
|
|
2
|
+
import mongoose, { model } from "mongoose";
|
|
3
|
+
import asyncHandler from "../../utils/asyncHandler.js";
|
|
4
|
+
import crudService from "./crud.service.js";
|
|
5
|
+
import registerModel from "../../lib/model.registry.js";
|
|
6
|
+
import AppError from "../../utils/AppError.js";
|
|
7
|
+
import zodValidations from "../../lib/zod.validations.js";
|
|
8
|
+
import z from "zod";
|
|
9
|
+
import protect from "../../middleware/auth.middleware.js";
|
|
10
|
+
|
|
11
|
+
const validateCookie = async (validations, user, isValidCookie) => {
|
|
12
|
+
const zodCookieObj = z.object(validations.cookieValidation);
|
|
13
|
+
isValidCookie = await zodCookieObj.safeParse(user);
|
|
14
|
+
|
|
15
|
+
if (!isValidCookie.success) {
|
|
16
|
+
const issue = isValidCookie.error.issues[0];
|
|
17
|
+
if (issue.code === "invalid_type")
|
|
18
|
+
throw new AppError(`${issue.path.join(".")} is required`);
|
|
19
|
+
|
|
20
|
+
throw new AppError(issue.message, 409);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return isValidCookie;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const crud = (route, routes, modelName, validations, apiVersion) => {
|
|
27
|
+
routes.forEach((el) => {
|
|
28
|
+
const middlewares = [];
|
|
29
|
+
if (el.method !== "get" || el.path !== "/") middlewares.push(protect);
|
|
30
|
+
if (el.middleware) middlewares.push(el.middleware);
|
|
31
|
+
if (el.middlewares) middlewares.push(...el.middlewares);
|
|
32
|
+
|
|
33
|
+
app[el.method](
|
|
34
|
+
`/api/v${apiVersion}/${route}${el.path}`,
|
|
35
|
+
...middlewares,
|
|
36
|
+
asyncHandler(async (req, res) => {
|
|
37
|
+
const Model = registerModel[modelName];
|
|
38
|
+
if (!Model && el.modelName)
|
|
39
|
+
throw new Error(`Model not found for endpoint: ${el.path}`);
|
|
40
|
+
|
|
41
|
+
// if not getAll and cookie - validation
|
|
42
|
+
let isValidCookie = undefined;
|
|
43
|
+
if (el.method !== "get" || el.path !== "/") {
|
|
44
|
+
isValidCookie = await validateCookie(
|
|
45
|
+
validations,
|
|
46
|
+
req.user,
|
|
47
|
+
isValidCookie,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// req.body - validation
|
|
52
|
+
const payload = {
|
|
53
|
+
...req.body,
|
|
54
|
+
image: req.file,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const validationObj =
|
|
58
|
+
validations && validations[el.handler]
|
|
59
|
+
? z.object(validations[el.handler])
|
|
60
|
+
: z.object({});
|
|
61
|
+
|
|
62
|
+
const zodBodyObj = validationObj;
|
|
63
|
+
|
|
64
|
+
const isValidBody = zodBodyObj.safeParse(payload || {});
|
|
65
|
+
|
|
66
|
+
if (!isValidBody.success) {
|
|
67
|
+
const issue = isValidBody.error.issues[0];
|
|
68
|
+
if (issue.code === "invalid_type")
|
|
69
|
+
throw new AppError(`${issue.path.join(".")} is required`);
|
|
70
|
+
|
|
71
|
+
throw new AppError(issue.message, 409);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// send data to service
|
|
75
|
+
const result = await crudService[el.handler]({
|
|
76
|
+
Model,
|
|
77
|
+
modelName,
|
|
78
|
+
body: isValidBody?.data,
|
|
79
|
+
userData: isValidCookie?.data,
|
|
80
|
+
id: req?.params?.id,
|
|
81
|
+
fileType: el?.fileType,
|
|
82
|
+
metaDataName: el?.metaDataName,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return res.json({
|
|
86
|
+
success: true,
|
|
87
|
+
data: result?.data,
|
|
88
|
+
message: result?.msg,
|
|
89
|
+
});
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default crud;
|