@starklabs/backend-core 1.1.1 → 1.2.1

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 (40) hide show
  1. package/.env.example +21 -0
  2. package/dist/js/config/cloudinary.js +18 -0
  3. package/dist/js/config/config.js +11 -0
  4. package/dist/js/config/duration.js +22 -0
  5. package/dist/js/core/app.js +122 -0
  6. package/dist/js/core/auth/OTP.js +115 -0
  7. package/dist/js/core/auth/auth.controller.js +63 -0
  8. package/dist/js/core/auth/auth.service.js +290 -0
  9. package/dist/js/core/auth/auth.validation.js +95 -0
  10. package/dist/js/core/crud/crud.controller.js +95 -0
  11. package/dist/js/core/crud/crud.service.js +296 -0
  12. package/dist/js/core/index.js +3 -0
  13. package/dist/js/index.js +44 -55
  14. package/dist/js/lib/db.js +40 -0
  15. package/dist/js/lib/field.types.js +174 -0
  16. package/dist/js/lib/model.factory.js +19 -0
  17. package/dist/js/lib/model.registry.js +4 -0
  18. package/dist/js/lib/schema.builder.js +35 -0
  19. package/dist/js/lib/zod.validations.js +247 -0
  20. package/dist/js/middleware/auth.middleware.js +51 -0
  21. package/dist/js/middleware/error.middleware.js +28 -0
  22. package/dist/js/middleware/socket.middleware.js +29 -0
  23. package/dist/js/utils/AppLog.js +2 -1
  24. package/dist/js/utils/deleteFile.js +22 -0
  25. package/dist/js/utils/index.js +10 -1
  26. package/dist/js/utils/jwt.js +12 -20
  27. package/dist/js/utils/libsodium.js +19 -3
  28. package/dist/js/utils/rateLimiter.js +25 -0
  29. package/dist/js/utils/uploadFile.js +43 -0
  30. package/handlerMap.js +33 -0
  31. package/package.json +17 -4
  32. package/dist/cjs/db.cjs +0 -17
  33. package/dist/cjs/index.cjs +0 -59
  34. package/dist/cjs/utils/AppError.cjs +0 -13
  35. package/dist/cjs/utils/AppLog.cjs +0 -13
  36. package/dist/cjs/utils/asyncHandler.cjs +0 -6
  37. package/dist/cjs/utils/jwt.cjs +0 -38
  38. package/dist/cjs/utils/libsodium.cjs +0 -145
  39. package/dist/cjs/utils/successResponse.cjs +0 -13
  40. package/dist/js/db.js +0 -19
@@ -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;
@@ -0,0 +1,296 @@
1
+ import { getConfig } from "../../config/config.js";
2
+ import { hash } from "../../utils/libsodium.js";
3
+ import { AppError, uploadFile, deleteFile } from "../../utils/index.js";
4
+
5
+ const sanitize = (doc) => {
6
+ if (!doc) return doc;
7
+ const { __v, password, ...clean } = doc;
8
+ return clean;
9
+ };
10
+
11
+ const authorizeAccess = ({ ownerEmail, dataEmail, role }) => {
12
+ const { internalRoles } = getConfig();
13
+
14
+ if (!internalRoles?.length)
15
+ throw new AppError("internalRoles is missing in StarkCore({})");
16
+
17
+ const isInternalRuler = internalRoles.includes(role);
18
+ const isOwner = ownerEmail === dataEmail;
19
+
20
+ if (!isInternalRuler && !isOwner) throw new AppError("Unauthorized", 401);
21
+ };
22
+
23
+ const assertInternalRole = (role) => {
24
+ const { internalRoles } = getConfig();
25
+
26
+ const isInternalRuler = internalRoles.includes(role);
27
+ if (!isInternalRuler) throw new AppError("Unauthorized", 401);
28
+ };
29
+
30
+ const showHealth = () => {
31
+ return {
32
+ msg: "All clear!",
33
+ };
34
+ };
35
+
36
+ const getAll = async ({ Model, modelName }) => {
37
+ const data = await Model.find().lean();
38
+
39
+ if (!data.length) {
40
+ throw new AppError(`${modelName} not found`, 404);
41
+ }
42
+
43
+ return {
44
+ data: data.map(sanitize),
45
+ msg: "Fetched all successfully",
46
+ };
47
+ };
48
+
49
+ const getById = async ({ Model, modelName, id, userData, body }) => {
50
+ const data = await Model.findById(id).lean();
51
+
52
+ if (!data) {
53
+ throw new AppError(`${modelName} with id ${id} not found`, 404);
54
+ }
55
+
56
+ authorizeAccess({
57
+ ownerEmail: userData.email,
58
+ dataEmail: data.email,
59
+ role: userData.role,
60
+ });
61
+
62
+ return {
63
+ data: sanitize(data),
64
+ msg: "Fetched successfully",
65
+ };
66
+ };
67
+
68
+ const create = async ({ Model, modelName, userData, body }) => {
69
+ assertInternalRole(userData.role);
70
+
71
+ const { cloudinaryFolderName } = getConfig();
72
+
73
+ // if image
74
+ let imageMetaData = undefined;
75
+ if (body.image) {
76
+ imageMetaData = await uploadFile(body.image, cloudinaryFolderName);
77
+ }
78
+
79
+ // if user
80
+ if (body.email) {
81
+ const exists = await Model.findOne({ email: body.email });
82
+ if (exists) {
83
+ throw new AppError(`${modelName} with this email already exists`, 409);
84
+ }
85
+ }
86
+
87
+ if (body.password) body.password = await hash(body.password);
88
+
89
+ const newItem = await Model.create({
90
+ ...body,
91
+ role: "user",
92
+ imageMetaData,
93
+ });
94
+
95
+ return {
96
+ data: sanitize(newItem.toObject()),
97
+ msg: "Created successfully",
98
+ };
99
+ };
100
+
101
+ const update = async ({ Model, modelName, id, body, userData }) => {
102
+ assertInternalRole(userData.role);
103
+
104
+ if (body.password) {
105
+ body.password = await hash(body.password);
106
+ }
107
+
108
+ const existing = await Model.findById(id);
109
+
110
+ if (!existing) {
111
+ throw new AppError(`${modelName} not found for update`, 404);
112
+ }
113
+
114
+ const { cloudinaryFolderName } = getConfig();
115
+
116
+ let imageMetaData;
117
+ if (body.image) {
118
+ imageMetaData = await uploadFile(body.image, cloudinaryFolderName);
119
+ }
120
+
121
+ try {
122
+ const replacementDoc = {
123
+ ...body,
124
+ ...(imageMetaData && { imageMetaData }),
125
+ };
126
+
127
+ // Prevent storing raw file object/base64/etc.
128
+ delete replacementDoc.image;
129
+
130
+ const updated = await Model.findOneAndReplace({ _id: id }, replacementDoc, {
131
+ returnDocument: "after",
132
+ }).lean();
133
+
134
+ // Delete old image after successful replacement
135
+ if (imageMetaData && existing.imageMetaData?.public_id) {
136
+ await deleteFile(existing.imageMetaData.public_id);
137
+ }
138
+
139
+ return {
140
+ data: sanitize(updated),
141
+ msg: "Updated successfully",
142
+ };
143
+ } catch (error) {
144
+ // Rollback newly uploaded image
145
+ if (imageMetaData?.public_id) {
146
+ await deleteFile(imageMetaData.public_id).catch(() => {});
147
+ }
148
+
149
+ if (error.message.includes("duplicate key error")) {
150
+ throw new AppError(`${modelName} with these detail already exists`);
151
+ }
152
+
153
+ throw error;
154
+ }
155
+ };
156
+
157
+ const patch = async ({ Model, modelName, id, body, userData }) => {
158
+ assertInternalRole(userData.role);
159
+
160
+ if (body.password) {
161
+ body.password = await hash(body.password);
162
+ }
163
+
164
+ const existing = await Model.findById(id);
165
+
166
+ if (!existing) {
167
+ throw new AppError(`${modelName} not found for patch`, 404);
168
+ }
169
+
170
+ const { cloudinaryFolderName } = getConfig();
171
+
172
+ let imageMetaData;
173
+ if (body.image) {
174
+ // Upload new image first
175
+ imageMetaData = await uploadFile(body.image, cloudinaryFolderName);
176
+ }
177
+
178
+ try {
179
+ const updated = await Model.findByIdAndUpdate(
180
+ id,
181
+ {
182
+ $set: {
183
+ ...body,
184
+ ...(imageMetaData && { imageMetaData }),
185
+ },
186
+ },
187
+ { returnDocument: "after" },
188
+ ).lean();
189
+
190
+ // Delete old image only after successful DB update
191
+ if (imageMetaData && existing.imageMetaData?.public_id) {
192
+ await deleteFile(existing.imageMetaData.public_id);
193
+ }
194
+
195
+ return {
196
+ data: sanitize(updated),
197
+ msg: "Updated successfully!",
198
+ };
199
+ } catch (error) {
200
+ // Rollback newly uploaded image if DB update fails
201
+ if (imageMetaData?.public_id) {
202
+ await deleteFile(imageMetaData.public_id).catch(() => {});
203
+ }
204
+
205
+ if (error.message.includes("duplicate key error")) {
206
+ throw new AppError(`${modelName} with these detail already exists`);
207
+ }
208
+
209
+ throw error;
210
+ }
211
+ };
212
+
213
+ const remove = async ({ Model, modelName, id, userData }) => {
214
+ assertInternalRole(userData.role);
215
+
216
+ const deleted = await Model.findByIdAndDelete(id).lean();
217
+
218
+ if (!deleted) {
219
+ throw new AppError(`${modelName} not found for delete`, 404);
220
+ }
221
+
222
+ return {
223
+ data: sanitize(deleted),
224
+ msg: "Deleted successfully",
225
+ };
226
+ };
227
+
228
+ const removeFile = async ({
229
+ Model,
230
+ modelName,
231
+ id,
232
+ userData,
233
+ fileType,
234
+ metaDataName,
235
+ }) => {
236
+ if (!fileType)
237
+ throw new AppError(
238
+ `fileType is required in ${modelName.toLowerCase()} collection`,
239
+ );
240
+
241
+ if (!metaDataName)
242
+ throw new AppError(
243
+ `metaDataName is required in ${modelName.toLowerCase()} collection`,
244
+ );
245
+
246
+ assertInternalRole(userData.role);
247
+
248
+ const item = await Model.findById(id);
249
+ if (!item) throw new AppError(`${modelName} not found to delete file`);
250
+
251
+ if (Object.keys(item[metaDataName]).length === 0)
252
+ throw new AppError(`${fileType} not found to delete`);
253
+
254
+ if (Object.keys(item[metaDataName].toObject()).length === 0)
255
+ throw new AppError(`${fileType} not found to delete`);
256
+
257
+ const deleted = await deleteFile(item[metaDataName].public_id, fileType);
258
+
259
+ item[metaDataName] = undefined;
260
+
261
+ item.save();
262
+
263
+ return {
264
+ data: item,
265
+ msg: "File deleted successfully",
266
+ };
267
+ };
268
+
269
+ const removeAll = async ({ Model, modelName, id, userData }) => {
270
+ assertInternalRole(userData.role);
271
+
272
+ const deleted = await Model.deleteMany();
273
+
274
+ if (!deleted) {
275
+ throw new AppError(`${modelName} not found for delete`, 404);
276
+ }
277
+
278
+ return {
279
+ data: sanitize(deleted),
280
+ msg: "Deleted all successfully",
281
+ };
282
+ };
283
+
284
+ // by deleting all, also delete the images from cloudinary 1/1
285
+
286
+ export default {
287
+ showHealth,
288
+ getAll,
289
+ getById,
290
+ create,
291
+ update,
292
+ patch,
293
+ remove,
294
+ removeFile,
295
+ removeAll,
296
+ };
@@ -0,0 +1,3 @@
1
+ export { startServer, app } from "./app.js";
2
+ export { default as crud } from "./crud/crud.controller.js";
3
+ export { default as auth } from "./auth/auth.controller.js";
package/dist/js/index.js CHANGED
@@ -1,66 +1,55 @@
1
- // connectDB - connects MongoDB database
2
- import connectDB from "./db.js";
1
+ import { setConfig } from "./config/config.js";
2
+
3
+ /**
4
+ * @typedef {Object} StarkCoreConfig
5
+ * @property {number} port - *Server listening port number*
6
+ * @property {Array<object>} collections - *Routes detail*
7
+ * @property {string} apiVersion - *Current API version*
8
+ * @property {string} jwtSecret - *Used for JSON Web Token functionalities*
9
+ * @property {boolean} isOffline - *Connects to local MongoDB instance*
10
+ * @property {string} masterKey - *Used for encryption/decryption operations*
11
+ * @property {string} ENV - *Current application environment (e.g. development, production)*
12
+ * @property {string} tokenExpiry - *JWT/token expiration duration*
13
+ * @property {Array<string>} internalRoles - *List of internal roles with elevated permissions*
14
+ * @property {string} resendAPIKey - *API key used for Resend email services*
15
+ * @property {string|number} rateLimitDuration - *Duration for rate limiting window*
16
+ * @property {number} maxReqLimit - *Maximum allowed requests within rate limit duration*
17
+ * @property {string} rateLimitMsg - *Message returned when rate limit is exceeded*
18
+ * @property {string} cloudinaryAPIKey - *Cloudinary API key*
19
+ * @property {string} cloudinaryCloudName - *Cloudinary cloud name*
20
+ * @property {string} cloudinaryAPISecret - *Cloudinary API secret*
21
+ * @property {string} cloudinaryFolderName - *Default Cloudinary folder for uploads*
22
+ */
23
+
24
+ class StarkCore {
25
+ /**
26
+ * @param {StarkCoreConfig} config
27
+ */
3
28
 
4
- // seal/unSeal - encrypt and decrypt data
5
- import { seal, unSeal } from "./utils/libsodium.js";
6
- import { signJWT, verifyJWT } from "./utils/jwt.js";
7
-
8
- class StarkAuth {
9
- #_masterKey;
10
- #_JWT_SECRET;
11
- #_JWT_EXPIRY;
12
-
13
- // constructor - store keys etc.
14
29
  constructor(config) {
15
- this.#_masterKey = config.MASTER_KEY;
16
- this.#_JWT_SECRET = config.JWT_SECRET;
17
- this.#_JWT_EXPIRY = config.JWT_EXPIRY;
18
- }
19
-
20
- // create - initializes DB and auth instance
21
- static async create(config) {
22
- await connectDB(config.MONGODB_URI, config.DB_NAME);
23
- return new StarkAuth(config);
24
- }
25
-
26
- // encrypt - encrypts string using master key
27
- async encrypt(str) {
28
- return seal(str, this.#_masterKey);
29
- }
30
-
31
- // decrypt - decrypts string using master key
32
- async decrypt(str, nonce, publicKey, securedPrivateKey) {
33
- return unSeal(str, nonce, publicKey, securedPrivateKey, this.#_masterKey);
30
+ setConfig(config);
34
31
  }
35
32
 
36
- // signJWT
37
- signJWT(payLoad) {
38
- return signJWT(payLoad, this.#_JWT_SECRET, this.#_JWT_EXPIRY);
39
- }
33
+ /**
34
+ * @param {StarkCoreConfig} config
35
+ * @returns {StarkCore}
36
+ */
40
37
 
41
- // verifyJWT
42
- verifyJWT(token) {
43
- return verifyJWT(token, this.#_JWT_SECRET);
38
+ static create(config) {
39
+ return new StarkCore(config);
44
40
  }
45
41
  }
46
42
 
47
- export default StarkAuth;
48
-
49
- // crypto - exports hashing utilities
50
- import { generateMasterKey, generateJWTSecret, hash, verifyHash } from "./utils/libsodium.js";
51
-
52
- export const crypto = {
53
- generateMasterKey,
43
+ export default StarkCore;
44
+ export { startServer } from "./core/index.js";
45
+ export { default as fieldTypes } from "./lib/field.types.js";
46
+ export { default as zodValidations } from "./lib/zod.validations.js";
47
+ export {
48
+ AppLog,
54
49
  generateJWTSecret,
50
+ generateMasterKey,
55
51
  hash,
52
+ seal,
53
+ unSeal,
56
54
  verifyHash,
57
- };
58
-
59
- // AppError - custom application error class
60
- export { default as AppError } from "./utils/AppError.js";
61
-
62
- // AppLog - structured logging utility
63
- export { default as AppLog } from "./utils/AppLog.js";
64
-
65
- // async handler - AppError in async functions
66
- export { default as asyncHandler } from "./utils/asyncHandler.js";
55
+ } from "./utils/index.js";
@@ -0,0 +1,40 @@
1
+ // module imports
2
+ import mongoose from "mongoose";
3
+
4
+ import AppLog from "../utils/AppLog.js";
5
+
6
+ const connectLocally = async () => {
7
+ await mongoose.connect(`mongodb://localhost:27017/offline-db`);
8
+ AppLog("check", "db", "Connected to OFFLINE-DB!");
9
+ };
10
+
11
+ // connecting to db
12
+ /**
13
+ *
14
+ * @param {"MONGODB_URI"} uri
15
+ * @param {"DATABASE_NAME"} database
16
+ * @param {true | false} NETWORK
17
+ * @returns
18
+ */
19
+ const connectDB = async (
20
+ MONGODB_URI = "",
21
+ DATABASE_NAME = "starklabs",
22
+ NETWORK = false,
23
+ ) => {
24
+ const mongodbUri = MONGODB_URI || "mongodb://localhost:27017";
25
+ try {
26
+ if (!NETWORK) {
27
+ await connectLocally();
28
+ return;
29
+ }
30
+
31
+ await mongoose.connect(`${mongodbUri}/${DATABASE_NAME}`);
32
+ AppLog("check", "db", "Connected successfully!");
33
+ } catch (error) {
34
+ AppLog("X", "db", "Error while connecting!");
35
+ AppLog("X", "db", error.message);
36
+ }
37
+ };
38
+
39
+ // export
40
+ export default connectDB;