@starklabs/backend-core 1.1.1 → 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 (41) 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/test.js +36 -0
  33. package/dist/cjs/db.cjs +0 -17
  34. package/dist/cjs/index.cjs +0 -59
  35. package/dist/cjs/utils/AppError.cjs +0 -13
  36. package/dist/cjs/utils/AppLog.cjs +0 -13
  37. package/dist/cjs/utils/asyncHandler.cjs +0 -6
  38. package/dist/cjs/utils/jwt.cjs +0 -38
  39. package/dist/cjs/utils/libsodium.cjs +0 -145
  40. package/dist/cjs/utils/successResponse.cjs +0 -13
  41. package/dist/js/db.js +0 -19
@@ -0,0 +1,174 @@
1
+ import mongoose from "mongoose";
2
+
3
+ const { ObjectId } = mongoose.Schema.Types;
4
+
5
+ const requiredString = {
6
+ type: String,
7
+ required: true,
8
+ trim: true,
9
+ };
10
+
11
+ const optionalString = {
12
+ type: String,
13
+ default: "",
14
+ trim: true,
15
+ };
16
+
17
+ const requiredUniqueString = {
18
+ type: String,
19
+ required: true,
20
+ unique: true,
21
+ trim: true,
22
+ };
23
+
24
+ const email = {
25
+ type: String,
26
+ required: true,
27
+ unique: true,
28
+ lowercase: true,
29
+ trim: true,
30
+ };
31
+
32
+ const password = {
33
+ type: String,
34
+ required: true,
35
+ minlength: 6,
36
+ select: false,
37
+ };
38
+
39
+ const requiredNumber = {
40
+ type: Number,
41
+ required: true,
42
+ };
43
+
44
+ const optionalNumber = {
45
+ type: Number,
46
+ default: 0,
47
+ };
48
+
49
+ const booleanTrue = {
50
+ type: Boolean,
51
+ default: true,
52
+ };
53
+
54
+ const booleanFalse = {
55
+ type: Boolean,
56
+ default: false,
57
+ };
58
+
59
+ const dateNow = {
60
+ type: Date,
61
+ default: Date.now,
62
+ };
63
+
64
+ const stringArray = {
65
+ type: [String],
66
+ default: [],
67
+ };
68
+
69
+ const objectArray = {
70
+ type: [Object],
71
+ default: [],
72
+ };
73
+
74
+ const objectId = {
75
+ type: ObjectId,
76
+ };
77
+
78
+ const requiredObjectId = {
79
+ type: ObjectId,
80
+ required: true,
81
+ };
82
+
83
+ const userRef = {
84
+ type: ObjectId,
85
+ ref: "User",
86
+ };
87
+
88
+ const requiredUserRef = {
89
+ type: ObjectId,
90
+ ref: "User",
91
+ required: true,
92
+ };
93
+
94
+ const userRefArray = [
95
+ {
96
+ type: ObjectId,
97
+ ref: "User",
98
+ },
99
+ ];
100
+
101
+ const timestamps = {
102
+ createdAt: dateNow,
103
+ updatedAt: dateNow,
104
+ };
105
+
106
+ const provider = {
107
+ type: String,
108
+ enum: ["local", "google"],
109
+ default: "local",
110
+ };
111
+
112
+ const role = {
113
+ ...requiredString,
114
+ enum: ["user", ...JSON.parse(process.env.INTERNAL_ROLES || "[]")],
115
+ default: "user",
116
+ };
117
+
118
+ const otp = {
119
+ type: String,
120
+ required: true,
121
+ };
122
+
123
+ const otpExpiry = {
124
+ type: Number,
125
+ required: true,
126
+ };
127
+
128
+ const otpCount = {
129
+ type: Number,
130
+ required: true,
131
+ default: 0,
132
+ max: [10, "OTP verification limit reached. Please try again later."],
133
+ };
134
+
135
+ const otpStatus = {
136
+ type: String,
137
+ enum: ["pending", "verified", "blocked"],
138
+ default: "pending",
139
+ };
140
+
141
+ const imageMetaData = {
142
+ public_id: { type: String, required: false },
143
+ secure_url: { type: String, required: false },
144
+ width: { type: Number, required: false },
145
+ height: { type: Number, required: false },
146
+ };
147
+
148
+ export default {
149
+ requiredString,
150
+ optionalString,
151
+ requiredUniqueString,
152
+ email,
153
+ password,
154
+ requiredNumber,
155
+ optionalNumber,
156
+ booleanTrue,
157
+ booleanFalse,
158
+ dateNow,
159
+ stringArray,
160
+ objectArray,
161
+ objectId,
162
+ requiredObjectId,
163
+ userRef,
164
+ requiredUserRef,
165
+ userRefArray,
166
+ timestamps,
167
+ provider,
168
+ role,
169
+ otp,
170
+ otpExpiry,
171
+ otpCount,
172
+ otpStatus,
173
+ imageMetaData,
174
+ };
@@ -0,0 +1,19 @@
1
+ import mongoose from "mongoose";
2
+ import buildSchema from "./schema.builder.js";
3
+ import AppLog from "../utils/AppLog.js";
4
+ import AppError from "../utils/AppError.js";
5
+
6
+ const createModel = (name, definition) => {
7
+ if (!name) throw new AppError("modelName is required");
8
+ if (!definition) throw new AppError("mongooseSchema is required");
9
+
10
+ const schema = buildSchema(definition);
11
+
12
+ if (mongoose.modelNames().includes(name)) {
13
+ return mongoose.model(name);
14
+ }
15
+ AppLog("db", "modelFactory", `${name} model created!`);
16
+ return mongoose.model(name, schema);
17
+ };
18
+
19
+ export default createModel;
@@ -0,0 +1,4 @@
1
+ // GLOBAL MODEL REGISTRY
2
+ const registerModel = {};
3
+
4
+ export default registerModel;
@@ -0,0 +1,35 @@
1
+ import mongoose from "mongoose";
2
+
3
+ const buildSchema = (definition) => {
4
+ const schemaObject = {};
5
+
6
+ for (const key in definition) {
7
+ const value = definition[key];
8
+
9
+ // CASE 1: primitive shorthand (String, Number, etc.)
10
+ if (
11
+ typeof value === "string" ||
12
+ typeof value === "number" ||
13
+ typeof value === "boolean"
14
+ ) {
15
+ schemaObject[key] = { type: value };
16
+ continue;
17
+ }
18
+
19
+ // CASE 2: already mongoose-style object
20
+ if (typeof value === "object" && !Array.isArray(value)) {
21
+ schemaObject[key] = value;
22
+ continue;
23
+ }
24
+
25
+ // CASE 3: array schema
26
+ if (Array.isArray(value)) {
27
+ schemaObject[key] = value;
28
+ continue;
29
+ }
30
+ }
31
+
32
+ return new mongoose.Schema(schemaObject, { timestamps: true });
33
+ };
34
+
35
+ export default buildSchema;
@@ -0,0 +1,247 @@
1
+ import { z } from "zod";
2
+ import { getConfig } from "../config/config.js";
3
+ import "dotenv/config";
4
+
5
+ /**
6
+ * =========================
7
+ * STRING HELPERS
8
+ * =========================
9
+ */
10
+
11
+ export const requiredString = z
12
+ .string()
13
+ .trim()
14
+ .min(1, "At least 1 character in string is required");
15
+
16
+ export const optionalString = z.string().trim().default("");
17
+
18
+ /**
19
+ * =========================
20
+ * EMAIL
21
+ * =========================
22
+ */
23
+
24
+ export const email = z
25
+ .string()
26
+ .trim()
27
+ .toLowerCase()
28
+ .email("Invalid email address");
29
+
30
+ /**
31
+ * =========================
32
+ * PASSWORD
33
+ * =========================
34
+ */
35
+
36
+ export const password = z
37
+ .string({
38
+ required_error: "Password is required",
39
+ invalid_type_error: "Password is required",
40
+ })
41
+ .min(6, "Password must be at least 6 characters");
42
+
43
+ /**
44
+ * =========================
45
+ * NUMBER HELPERS
46
+ * =========================
47
+ */
48
+
49
+ export const requiredNumber = z.coerce
50
+ .number()
51
+ .min(1, "Number must be 1 or greater");
52
+
53
+ export const optionalNumber = z.coerce.number().default(0);
54
+
55
+ /**
56
+ * =========================
57
+ * BOOLEAN HELPERS
58
+ * =========================
59
+ */
60
+
61
+ export const booleanTrue = z.boolean().default(true);
62
+ export const booleanFalse = z.boolean().default(false);
63
+
64
+ /**
65
+ * =========================
66
+ * DATE
67
+ * =========================
68
+ */
69
+
70
+ export const dateNow = z.date().default(() => new Date());
71
+
72
+ /**
73
+ * =========================
74
+ * ARRAYS
75
+ * =========================
76
+ */
77
+
78
+ export const stringArray = z.preprocess((val) => {
79
+ if (Array.isArray(val)) return val;
80
+
81
+ if (typeof val === "string") {
82
+ try {
83
+ const parsed = JSON.parse(val);
84
+ if (Array.isArray(parsed)) return parsed;
85
+ } catch (e) {
86
+ return val.split(",").map((v) => v.trim());
87
+ }
88
+ }
89
+
90
+ return [];
91
+ }, z.array(z.string()).default([]));
92
+
93
+ export const requiredStringArray = z.preprocess(
94
+ (val) => {
95
+ // already correct
96
+ if (Array.isArray(val)) return val;
97
+
98
+ // string case (your problem case)
99
+ if (typeof val === "string") {
100
+ try {
101
+ const parsed = JSON.parse(val);
102
+ if (Array.isArray(parsed)) return parsed;
103
+ } catch (e) {
104
+ // fallback: treat as comma-separated string
105
+ return val.split(",").map((v) => v.trim());
106
+ }
107
+ }
108
+
109
+ // weird multer case: array of broken strings
110
+ if (Array.isArray(val)) {
111
+ const joined = val.join("");
112
+ try {
113
+ const parsed = JSON.parse(joined);
114
+ if (Array.isArray(parsed)) return parsed;
115
+ } catch (e) {
116
+ return val;
117
+ }
118
+ }
119
+
120
+ return [];
121
+ },
122
+ z.array(z.string()).min(1, "Array must have at least 1 element"),
123
+ );
124
+
125
+ export const objectArray = z.preprocess(
126
+ (val) => {
127
+ if (Array.isArray(val)) return val;
128
+
129
+ if (typeof val === "string") {
130
+ try {
131
+ const parsed = JSON.parse(val);
132
+ if (Array.isArray(parsed)) return parsed;
133
+ } catch (e) {
134
+ return [];
135
+ }
136
+ }
137
+
138
+ return [];
139
+ },
140
+ z.array(z.object({})).default([]),
141
+ );
142
+
143
+ export const requiredObjectArray = z.preprocess(
144
+ (val) => {
145
+ // already correct
146
+ if (Array.isArray(val)) return val;
147
+
148
+ // string case (JSON from form-data)
149
+ if (typeof val === "string") {
150
+ try {
151
+ const parsed = JSON.parse(val);
152
+ if (Array.isArray(parsed)) return parsed;
153
+ } catch (e) {
154
+ return [];
155
+ }
156
+ }
157
+
158
+ return [];
159
+ },
160
+ z.array(z.object({})).min(1, "Array must have at least 1 object"),
161
+ );
162
+
163
+ /**
164
+ * =========================
165
+ * OBJECT ID / REFERENCES
166
+ * =========================
167
+ */
168
+
169
+ export const objectId = z
170
+ .string({
171
+ required_error: "ObjectId is required",
172
+ invalid_type_error: "Invalid ObjectId",
173
+ })
174
+ .regex(/^[0-9a-fA-F]{24}$/, "Invalid ObjectId");
175
+
176
+ export const requiredObjectId = objectId;
177
+
178
+ export const userRef = objectId;
179
+ export const requiredUserRef = objectId;
180
+
181
+ export const userRefArray = z.array(objectId).default([]);
182
+
183
+ /**
184
+ * =========================
185
+ * ENUMS
186
+ * =========================
187
+ */
188
+
189
+ export const provider = z.enum(["local", "google"]).default("local");
190
+
191
+ const internalRoles = JSON.parse(process.env.INTERNAL_ROLES || "[]");
192
+
193
+ export const role = z.enum(["user", ...internalRoles]).default("user");
194
+
195
+ /**
196
+ * =========================
197
+ * TIMESTAMPS
198
+ * =========================
199
+ */
200
+
201
+ export const timestamps = {
202
+ createdAt: dateNow,
203
+ updatedAt: dateNow,
204
+ };
205
+
206
+ // files
207
+ const requiredImage = z
208
+ .object({
209
+ fieldname: z.string(),
210
+ originalname: z.string(),
211
+ mimetype: z.string(),
212
+ size: z.number(),
213
+ })
214
+ .passthrough()
215
+ .refine((file) => file.mimetype.startsWith("image/"), {
216
+ message: "Only image files are allowed",
217
+ });
218
+ /**
219
+ * =========================
220
+ * EXPORT ALL
221
+ * =========================
222
+ */
223
+
224
+ export default {
225
+ requiredString,
226
+ optionalString,
227
+ email,
228
+ password,
229
+ requiredNumber,
230
+ optionalNumber,
231
+ booleanTrue,
232
+ booleanFalse,
233
+ dateNow,
234
+ stringArray,
235
+ requiredStringArray,
236
+ objectArray,
237
+ requiredObjectArray,
238
+ objectId,
239
+ requiredObjectId,
240
+ userRef,
241
+ requiredUserRef,
242
+ userRefArray,
243
+ timestamps,
244
+ provider,
245
+ role,
246
+ requiredImage,
247
+ };
@@ -0,0 +1,51 @@
1
+ import { verifyJWT, AppError } from "../utils/index.js";
2
+ import registerModel from "../lib/model.registry.js";
3
+ import { getConfig } from "../config/config.js";
4
+
5
+ // find user safely
6
+ const findUser = async (email) => {
7
+ const { userModel } = getConfig();
8
+ const Model = registerModel[userModel];
9
+
10
+ return await Model.findOne({ email });
11
+ };
12
+
13
+ // protect middleware
14
+ const protect = async (req, res, next) => {
15
+ try {
16
+ const token = req.cookies?.authToken;
17
+
18
+ if (!token) {
19
+ return next(new AppError("Authentication required", 401));
20
+ }
21
+
22
+ const { jwtSecret } = getConfig();
23
+
24
+ const payload = verifyJWT(token, jwtSecret);
25
+
26
+ if (!payload?.email) {
27
+ return next(new AppError("Invalid token payload", 401));
28
+ }
29
+
30
+ const user = await findUser(payload.email);
31
+
32
+ if (!user) {
33
+ return next(new AppError("This account no longer exists", 401));
34
+ }
35
+
36
+ // optional: ensure provider consistency
37
+ if (payload.provider && payload.provider !== user.provider) {
38
+ return next(new AppError("Provider mismatch", 401));
39
+ }
40
+
41
+ // attach clean user (not raw JWT)
42
+ const { iat, exp, ...cleanPayload } = payload;
43
+ req.user = { ...cleanPayload, authProvider: user?.provider };
44
+
45
+ next();
46
+ } catch (error) {
47
+ return next(new AppError("Unauthorized", 401));
48
+ }
49
+ };
50
+
51
+ export default protect;
@@ -0,0 +1,28 @@
1
+ // custom imports
2
+ import AppLog from "../utils/AppLog.js";
3
+
4
+ const errorMiddleware = (err, req, res, next) => {
5
+ let statusCode = err.statusCode || 500;
6
+ let message = err.message || "Internal Server Error";
7
+
8
+ // Log everything for debugging
9
+ AppLog("X", "error.middleware.js", message);
10
+ console.log(err);
11
+
12
+ // Handle known / operational errors
13
+ if (err.isOperational) {
14
+ return res.status(statusCode).json({
15
+ success: false,
16
+ message,
17
+ });
18
+ }
19
+
20
+
21
+ // Unknown / programming errors
22
+ return res.status(500).json({
23
+ success: false,
24
+ message: "Something went wrong",
25
+ });
26
+ };
27
+
28
+ export default errorMiddleware;
@@ -0,0 +1,29 @@
1
+ // module imports
2
+ import cookie from "cookie";
3
+
4
+ // custom imports
5
+ import envs from "../config/envs.js";
6
+ import { verifyJWT, AppError } from "../utils/index.js";
7
+
8
+ export const socketProtect = (io) => {
9
+ io.use(async (socket, next) => {
10
+ try {
11
+ const cookies = cookie.parse(socket.handshake.headers.cookie || "");
12
+ const token = cookies.authToken;
13
+ if (!token) throw new AppError("Unauthorized");
14
+
15
+ const payLoad = verifyJWT(token);
16
+ delete payLoad.iat;
17
+ delete payLoad.exp;
18
+
19
+ payLoad.user = payLoad.id;
20
+
21
+ delete payLoad.id;
22
+
23
+ socket.user = payLoad;
24
+ next();
25
+ } catch (error) {
26
+ return next(new Error("Unauthorized"));
27
+ }
28
+ });
29
+ };
@@ -2,12 +2,13 @@
2
2
  const emojis = {
3
3
  check: "✅",
4
4
  X: "❌",
5
+ db: "📚"
5
6
  };
6
7
 
7
8
  // console logs
8
9
  /**
9
10
  *
10
- * @param {check | X} emoji
11
+ * @param {check | X | db} emoji
11
12
  * @param {string} file
12
13
  * @param {string} message
13
14
  */
@@ -0,0 +1,22 @@
1
+ import { cloudinary } from "../config/cloudinary.js";
2
+ import AppError from "./AppError.js";
3
+
4
+ const deleteFile = async (id, fileType) => {
5
+ try {
6
+ if (!fileType) throw new Error(`fileType is required`);
7
+ if (!id) throw new Error(`${fileType} public_id is required`);
8
+
9
+ const item = await cloudinary.uploader.destroy(id, {
10
+ resource_type: fileType,
11
+ });
12
+
13
+ if (item.result === "not found")
14
+ throw new AppError("File not found to delete");
15
+
16
+ return true;
17
+ } catch (error) {
18
+ throw new AppError(error.message);
19
+ }
20
+ };
21
+
22
+ export default deleteFile;
@@ -4,4 +4,13 @@ export { default as successResponse } from "./successResponse.js";
4
4
  export { default as asyncHandler } from "./asyncHandler.js";
5
5
  export { default as AppError } from "./AppError.js";
6
6
  export { signJWT, verifyJWT } from "./jwt.js";
7
- export { hashPassword, verifyPassword } from "./libsodium.js";
7
+ export {
8
+ hash,
9
+ verifyHash,
10
+ seal,
11
+ unSeal,
12
+ generateJWTSecret,
13
+ generateMasterKey,
14
+ } from "./libsodium.js";
15
+ export { default as uploadFile } from "./uploadFile.js";
16
+ export {default as deleteFile} from "./deleteFile.js"
@@ -1,27 +1,16 @@
1
1
  import jwt from "jsonwebtoken";
2
2
  import AppError from "./AppError.js";
3
+ import getDuration from "../config/duration.js";
3
4
 
4
- export const EXPIRY = {
5
- "2m": 1000 * 60 * 2,
6
- "10m": 1000 * 60 * 10,
7
- "1h": 1000 * 60 * 60,
8
- "6h": 1000 * 60 * 60 * 6,
9
- "12h": 1000 * 60 * 60 * 12,
10
- "1d": 1000 * 60 * 60 * 24,
11
- "3d": 1000 * 60 * 60 * 24 * 3,
12
- "7d": 1000 * 60 * 60 * 24 * 7,
13
- "14d": 1000 * 60 * 60 * 24 * 14,
14
- "30d": 1000 * 60 * 60 * 24 * 30,
15
- };
16
-
17
- const getExpiry = (key) => {
18
- if (!EXPIRY[key]) throw new Error("Invalid expiry key");
19
- return EXPIRY[key];
20
- };
21
-
22
- const signJWT = (payLoad, JWT_SECRET, JWT_EXPIRY) => {
5
+ const signJWT = (payLoad, JWT_SECRET, tokenExpiry) => {
23
6
  try {
24
- return jwt.sign(payLoad, JWT_SECRET, { expiresIn: JWT_EXPIRY });
7
+ if (!payLoad) throw new Error("payLoad is required");
8
+ if (!JWT_SECRET) throw new Error("jwtSecret is required");
9
+ if (!tokenExpiry) throw new Error("tokenExpiry is required");
10
+
11
+ return jwt.sign(payLoad, JWT_SECRET, {
12
+ expiresIn: getDuration(tokenExpiry),
13
+ });
25
14
  } catch (error) {
26
15
  throw new AppError(error.message);
27
16
  }
@@ -29,6 +18,9 @@ const signJWT = (payLoad, JWT_SECRET, JWT_EXPIRY) => {
29
18
 
30
19
  const verifyJWT = (token, JWT_SECRET) => {
31
20
  try {
21
+ if (!JWT_SECRET) throw new Error("jwtSecret is required");
22
+ if (!token) throw new Error("JWT token is required");
23
+
32
24
  return jwt.verify(token, JWT_SECRET);
33
25
  } catch (error) {
34
26
  throw new AppError("Invalid or expired token", 401);