@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.
Files changed (42) hide show
  1. package/.env.example +21 -0
  2. package/README.md +7 -7
  3. package/dist/js/config/cloudinary.js +18 -0
  4. package/dist/js/config/config.js +11 -0
  5. package/dist/js/config/duration.js +22 -0
  6. package/dist/js/core/app.js +122 -0
  7. package/dist/js/core/auth/OTP.js +115 -0
  8. package/dist/js/core/auth/auth.controller.js +63 -0
  9. package/dist/js/core/auth/auth.service.js +290 -0
  10. package/dist/js/core/auth/auth.validation.js +95 -0
  11. package/dist/js/core/crud/crud.controller.js +95 -0
  12. package/dist/js/core/crud/crud.service.js +296 -0
  13. package/dist/js/core/index.js +3 -0
  14. package/dist/js/index.js +44 -55
  15. package/dist/js/lib/db.js +40 -0
  16. package/dist/js/lib/field.types.js +174 -0
  17. package/dist/js/lib/model.factory.js +19 -0
  18. package/dist/js/lib/model.registry.js +4 -0
  19. package/dist/js/lib/schema.builder.js +35 -0
  20. package/dist/js/lib/zod.validations.js +247 -0
  21. package/dist/js/middleware/auth.middleware.js +51 -0
  22. package/dist/js/middleware/error.middleware.js +28 -0
  23. package/dist/js/middleware/socket.middleware.js +29 -0
  24. package/dist/js/utils/AppLog.js +2 -1
  25. package/dist/js/utils/deleteFile.js +22 -0
  26. package/dist/js/utils/index.js +10 -1
  27. package/dist/js/utils/jwt.js +12 -20
  28. package/dist/js/utils/libsodium.js +19 -3
  29. package/dist/js/utils/rateLimiter.js +25 -0
  30. package/dist/js/utils/uploadFile.js +43 -0
  31. package/handlerMap.js +33 -0
  32. package/package.json +18 -5
  33. package/test.js +36 -0
  34. package/dist/cjs/db.cjs +0 -17
  35. package/dist/cjs/index.cjs +0 -59
  36. package/dist/cjs/utils/AppError.cjs +0 -13
  37. package/dist/cjs/utils/AppLog.cjs +0 -13
  38. package/dist/cjs/utils/asyncHandler.cjs +0 -6
  39. package/dist/cjs/utils/jwt.cjs +0 -38
  40. package/dist/cjs/utils/libsodium.cjs +0 -145
  41. package/dist/cjs/utils/successResponse.cjs +0 -13
  42. 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;