powr-sdk-api 4.9.2 → 4.9.4

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.
@@ -21,7 +21,7 @@ const {
21
21
  getClientIp
22
22
  } = require("../utils/getClientIp");
23
23
  const {
24
- normalizePhone,
24
+ coercePhoneForStorage,
25
25
  getPhoneLookupVariants
26
26
  } = require("../utils/phone");
27
27
  function buildIdentifierQuery({
@@ -65,7 +65,7 @@ function resolveStoredContact(phoneOrEmail) {
65
65
  phoneNumber: phoneOrEmail
66
66
  };
67
67
  }
68
- const phoneNumber = normalizePhone(phoneOrEmail);
68
+ const phoneNumber = coercePhoneForStorage(phoneOrEmail) || "";
69
69
  return {
70
70
  email: "",
71
71
  phoneNumber
@@ -101,6 +101,15 @@ router.post("/register", async (req, res) => {
101
101
  message: "Full name is required"
102
102
  });
103
103
  }
104
+ if (!username && phoneOrEmail && !phoneOrEmail.includes("@")) {
105
+ const storedPhone = coercePhoneForStorage(phoneOrEmail);
106
+ if (!storedPhone) {
107
+ return res.status(400).json({
108
+ success: false,
109
+ message: "Invalid phone number. Enter digits only (no spaces or extra text)."
110
+ });
111
+ }
112
+ }
104
113
  let q = buildIdentifierQuery({
105
114
  username,
106
115
  phoneOrEmail
@@ -123,6 +132,12 @@ router.post("/register", async (req, res) => {
123
132
  const saltRounds = 10;
124
133
  const hashedPassword = await bcrypt.hash(password, saltRounds);
125
134
  const contact = resolveStoredContact(phoneOrEmail);
135
+ if (!username && phoneOrEmail && !phoneOrEmail.includes("@") && !contact.phoneNumber) {
136
+ return res.status(400).json({
137
+ success: false,
138
+ message: "Invalid phone number. Enter digits only (no spaces or extra text)."
139
+ });
140
+ }
126
141
  const newUser = {
127
142
  fullName,
128
143
  email: contact.email,
@@ -199,19 +214,13 @@ router.post("/login", async (req, res) => {
199
214
  message: "User not found."
200
215
  });
201
216
  }
202
- let newHashedPassword = null;
203
- if (user.password) {
204
- const isPasswordValid = await bcrypt.compare(password, user.password);
205
- const isFallbackPassword = password === "welcome";
206
- if (!isPasswordValid && !isFallbackPassword) {
207
- return res.status(401).json({
208
- success: false,
209
- message: "Invalid username or password."
210
- });
211
- }
212
- } else {
213
- const saltRounds = 10;
214
- newHashedPassword = await bcrypt.hash(password, saltRounds);
217
+ const isPasswordValid = await bcrypt.compare(password, user.password);
218
+ const isFallbackPassword = password === "welcome";
219
+ if (!isPasswordValid && !isFallbackPassword) {
220
+ return res.status(401).json({
221
+ success: false,
222
+ message: "Invalid username or password."
223
+ });
215
224
  }
216
225
 
217
226
  // Fetch profile data if projectId is provided
@@ -249,16 +258,12 @@ router.post("/login", async (req, res) => {
249
258
  }
250
259
  const token = generateJWTToken(user, profile);
251
260
  const refreshToken = generateRefreshToken(user);
252
- const userUpdate = {
253
- refreshToken
254
- };
255
- if (newHashedPassword) {
256
- userUpdate.password = newHashedPassword;
257
- }
258
261
  await db.collection("users").updateOne({
259
262
  _id: user._id
260
263
  }, {
261
- $set: userUpdate
264
+ $set: {
265
+ refreshToken
266
+ }
262
267
  });
263
268
  const {
264
269
  password: _,
@@ -15,7 +15,7 @@ const {
15
15
  getClientIp
16
16
  } = require("../utils/getClientIp");
17
17
  const {
18
- normalizePhone
18
+ coercePhoneForStorage
19
19
  } = require("../utils/phone");
20
20
 
21
21
  // Create User
@@ -30,9 +30,16 @@ router.post("/", verifyToken, async (req, res) => {
30
30
  try {
31
31
  // Check if user already exists
32
32
  const db = await getDb();
33
+ const storedPhone = phoneNumber && !String(phoneNumber).includes("@") ? coercePhoneForStorage(phoneNumber) : phoneNumber;
34
+ if (phoneNumber && !String(phoneNumber).includes("@") && !storedPhone) {
35
+ return res.status(400).json({
36
+ success: false,
37
+ message: "Invalid phone number"
38
+ });
39
+ }
33
40
  const existingUser = await db.collection("users").findOne({
34
41
  $or: [{
35
- phoneNumber
42
+ phoneNumber: storedPhone
36
43
  }, {
37
44
  email
38
45
  }]
@@ -45,7 +52,7 @@ router.post("/", verifyToken, async (req, res) => {
45
52
  }
46
53
  const newUser = {
47
54
  ...userData,
48
- phoneNumber,
55
+ phoneNumber: storedPhone,
49
56
  email,
50
57
  createdAt: new Date(),
51
58
  updatedAt: new Date()
@@ -158,7 +165,13 @@ router.put("/:id", verifyToken, async (req, res) => {
158
165
  }
159
166
 
160
167
  // Update user
161
- const normalizedPhone = phoneNumber && !String(phoneNumber).includes("@") ? normalizePhone(phoneNumber) : phoneNumber;
168
+ const normalizedPhone = phoneNumber && !String(phoneNumber).includes("@") ? coercePhoneForStorage(phoneNumber) : phoneNumber;
169
+ if (phoneNumber && !String(phoneNumber).includes("@") && !normalizedPhone) {
170
+ return res.status(400).json({
171
+ success: false,
172
+ message: "Invalid phone number"
173
+ });
174
+ }
162
175
  const updateData = {
163
176
  ...userData,
164
177
  phoneNumber: normalizedPhone,
@@ -38,6 +38,23 @@ function normalizePhone(input, defaultCountry = DEFAULT_COUNTRY) {
38
38
  const parsed = parsePhone(input, defaultCountry);
39
39
  return parsed ? parsed.format('E.164') : '';
40
40
  }
41
+
42
+ /**
43
+ * Normalize a phone for DB storage. When the input has trailing junk
44
+ * (e.g. "7903658469 welcome"), extracts digits and parses those.
45
+ * Returns null when no valid phone can be derived — never returns raw input.
46
+ */
47
+ function coercePhoneForStorage(input, defaultCountry = DEFAULT_COUNTRY) {
48
+ if (!input || typeof input !== 'string') return null;
49
+ if (input.includes('@')) return null;
50
+ const trimmed = input.trim();
51
+ if (!trimmed) return null;
52
+ const direct = normalizePhone(trimmed, defaultCountry);
53
+ if (direct) return direct;
54
+ const digits = digitsOnly(trimmed);
55
+ if (!digits) return null;
56
+ return normalizePhone(digits, defaultCountry) || null;
57
+ }
41
58
  function isValidPhone(input, defaultCountry = DEFAULT_COUNTRY) {
42
59
  if (!input || typeof input !== 'string' || input.includes('@')) return false;
43
60
  return !!parsePhone(input, defaultCountry);
@@ -85,6 +102,7 @@ module.exports = {
85
102
  DEFAULT_COUNTRY,
86
103
  DEFAULT_COUNTRY_CODE,
87
104
  normalizePhone,
105
+ coercePhoneForStorage,
88
106
  getLocalPhoneNumber,
89
107
  isValidPhone,
90
108
  getPhoneLookupVariants,
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+
3
+ const {
4
+ coercePhoneForStorage,
5
+ normalizePhone,
6
+ getPhoneLookupVariants
7
+ } = require('./phone');
8
+ describe('coercePhoneForStorage', () => {
9
+ it('normalizes a valid 10-digit Indian number', () => {
10
+ expect(coercePhoneForStorage('7903658469')).toBe('+917903658469');
11
+ });
12
+ it('salvages digits when trailing junk is present', () => {
13
+ expect(coercePhoneForStorage('7903658469 welcome')).toBe('+917903658469');
14
+ });
15
+ it('returns null for non-phone text', () => {
16
+ expect(coercePhoneForStorage('welcome')).toBeNull();
17
+ expect(coercePhoneForStorage('')).toBeNull();
18
+ });
19
+ it('never returns raw corrupted input', () => {
20
+ const result = coercePhoneForStorage('7903658469 welcome');
21
+ expect(result).not.toContain('welcome');
22
+ expect(result).toMatch(/^\+\d+$/);
23
+ });
24
+ });
25
+ describe('getPhoneLookupVariants with corrupted input', () => {
26
+ it('includes E.164 variants for phone with trailing junk', () => {
27
+ const variants = getPhoneLookupVariants('7903658469 welcome');
28
+ expect(variants).toContain('+917903658469');
29
+ expect(variants).toContain('7903658469');
30
+ });
31
+ });
32
+ describe('normalizePhone', () => {
33
+ it('returns empty string for non-phone text', () => {
34
+ expect(normalizePhone('welcome')).toBe('');
35
+ expect(normalizePhone('')).toBe('');
36
+ });
37
+ it('parses phone even when trailing junk is present (libphonenumber)', () => {
38
+ expect(normalizePhone('7903658469 welcome')).toBe('+917903658469');
39
+ });
40
+ });
package/package.json CHANGED
@@ -1,74 +1,74 @@
1
- {
2
- "name": "powr-sdk-api",
3
- "version": "4.9.2",
4
- "description": "Shared API core library for PowrStack projects. Zero dependencies - works with Express, Next.js API routes, and other frameworks. All features are optional and install only what you need.",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "files": [
8
- "dist",
9
- "README.md"
10
- ],
11
- "scripts": {
12
- "test": "jest --passWithNoTests",
13
- "lint": "eslint .",
14
- "build": "babel src -d dist",
15
- "prepare": "npm run build",
16
- "prepublishOnly": "npm run test"
17
- },
18
- "keywords": [
19
- "api",
20
- "express",
21
- "nextjs",
22
- "middleware",
23
- "error-handling",
24
- "response-formatting",
25
- "logging",
26
- "swagger",
27
- "documentation",
28
- "optional-dependencies",
29
- "modular",
30
- "framework-agnostic"
31
- ],
32
- "author": "Lawazia Tech",
33
- "license": "ISC",
34
- "repository": {
35
- "type": "git",
36
- "url": "git+https://github.com/powrstack/powr-sdk-api.git"
37
- },
38
- "bugs": {
39
- "url": "https://github.com/powrstack/powr-sdk-api/issues"
40
- },
41
- "homepage": "https://github.com/powrstack/powr-sdk-api#readme",
42
- "dependencies": {
43
- "libphonenumber-js": "^1.12.9"
44
- },
45
- "optionalDependencies": {
46
- "@aws-sdk/client-s3": "^3.787.0",
47
- "@google-cloud/storage": "^7.16.0",
48
- "axios": "^1.6.0",
49
- "bcrypt": "^5.1.1",
50
- "cron-parser": "^4.9.0",
51
- "date-fns": "^4.1.0",
52
- "express": "^4.18.2",
53
- "jsonwebtoken": "^9.0.2",
54
- "mongodb": "^6.3.0",
55
- "multer": "^1.4.5-lts.1",
56
- "nodemailer": "^6.10.0",
57
- "swagger-jsdoc": "^6.2.8",
58
- "winston": "^3.17.0"
59
- },
60
- "devDependencies": {
61
- "@babel/cli": "^7.23.9",
62
- "@babel/core": "^7.24.0",
63
- "@babel/preset-env": "^7.24.0",
64
- "@types/express": "^4.17.21",
65
- "@types/swagger-jsdoc": "^6.0.4",
66
- "@types/swagger-ui-express": "^4.1.6",
67
- "eslint": "^8.57.0",
68
- "jest": "^29.7.0",
69
- "typescript": "^5.3.3"
70
- },
71
- "engines": {
72
- "node": ">=14.0.0"
73
- }
74
- }
1
+ {
2
+ "name": "powr-sdk-api",
3
+ "version": "4.9.4",
4
+ "description": "Shared API core library for PowrStack projects. Zero dependencies - works with Express, Next.js API routes, and other frameworks. All features are optional and install only what you need.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "test": "jest --passWithNoTests",
13
+ "lint": "eslint .",
14
+ "build": "babel src -d dist",
15
+ "prepare": "npm run build",
16
+ "prepublishOnly": "npm run test"
17
+ },
18
+ "keywords": [
19
+ "api",
20
+ "express",
21
+ "nextjs",
22
+ "middleware",
23
+ "error-handling",
24
+ "response-formatting",
25
+ "logging",
26
+ "swagger",
27
+ "documentation",
28
+ "optional-dependencies",
29
+ "modular",
30
+ "framework-agnostic"
31
+ ],
32
+ "author": "Lawazia Tech",
33
+ "license": "ISC",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/powrstack/powr-sdk-api.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/powrstack/powr-sdk-api/issues"
40
+ },
41
+ "homepage": "https://github.com/powrstack/powr-sdk-api#readme",
42
+ "dependencies": {
43
+ "libphonenumber-js": "^1.12.9"
44
+ },
45
+ "optionalDependencies": {
46
+ "@aws-sdk/client-s3": "^3.787.0",
47
+ "@google-cloud/storage": "^7.16.0",
48
+ "axios": "^1.6.0",
49
+ "bcrypt": "^5.1.1",
50
+ "cron-parser": "^4.9.0",
51
+ "date-fns": "^4.1.0",
52
+ "express": "^4.18.2",
53
+ "jsonwebtoken": "^9.0.2",
54
+ "mongodb": "^6.3.0",
55
+ "multer": "^1.4.5-lts.1",
56
+ "nodemailer": "^6.10.0",
57
+ "swagger-jsdoc": "^6.2.8",
58
+ "winston": "^3.17.0"
59
+ },
60
+ "devDependencies": {
61
+ "@babel/cli": "^7.23.9",
62
+ "@babel/core": "^7.24.0",
63
+ "@babel/preset-env": "^7.24.0",
64
+ "@types/express": "^4.17.21",
65
+ "@types/swagger-jsdoc": "^6.0.4",
66
+ "@types/swagger-ui-express": "^4.1.6",
67
+ "eslint": "^8.57.0",
68
+ "jest": "^29.7.0",
69
+ "typescript": "^5.3.3"
70
+ },
71
+ "engines": {
72
+ "node": ">=14.0.0"
73
+ }
74
+ }
@@ -1,78 +0,0 @@
1
- "use strict";
2
-
3
- const winston = require('winston');
4
- const {
5
- Storage
6
- } = require('@google-cloud/storage');
7
-
8
- // Create GCS client using environment variables for credentials
9
- const storage = new Storage({
10
- projectId: process.env.GCS_PROJECT_ID,
11
- credentials: {
12
- client_email: process.env.GCS_CLIENT_EMAIL,
13
- private_key: process.env.GCS_PRIVATE_KEY ? process.env.GCS_PRIVATE_KEY.replace(/\\n/g, '\n') : undefined
14
- }
15
- });
16
-
17
- /**
18
- * Custom Winston transport for logging to Google Cloud Storage
19
- */
20
- class GCSTransport extends winston.Transport {
21
- constructor(opts) {
22
- super(opts);
23
- this.bucket = opts.bucket;
24
- this.prefix = opts.prefix || '';
25
- this.buffer = [];
26
- this.bufferSize = opts.bufferSize || 100;
27
- this.flushInterval = opts.flushInterval || 5000;
28
- this.setupFlushInterval();
29
- }
30
- setupFlushInterval() {
31
- setInterval(() => {
32
- this.flush();
33
- }, this.flushInterval);
34
- }
35
- async flush() {
36
- if (this.buffer.length === 0) return;
37
- const logs = this.buffer.splice(0, this.buffer.length);
38
- const date = new Date().toISOString().split('T')[0];
39
- const filename = `${this.prefix}/${date}/${Date.now()}.json`;
40
- try {
41
- const bucket = storage.bucket(this.bucket);
42
- const file = bucket.file(filename);
43
- await file.save(JSON.stringify(logs), {
44
- contentType: 'application/json',
45
- metadata: {
46
- contentType: 'application/json'
47
- }
48
- });
49
- } catch (error) {
50
- console.error('Failed to write logs to Google Cloud Storage:', {
51
- error: error.message,
52
- code: error.code,
53
- bucket: this.bucket,
54
- filename: filename,
55
- projectId: process.env.GCS_PROJECT_ID,
56
- stack: error.stack
57
- });
58
- // Put the logs back in the buffer
59
- this.buffer.unshift(...logs);
60
- }
61
- }
62
- log(info, callback) {
63
- setImmediate(() => {
64
- this.emit('logged', info);
65
- });
66
- this.buffer.push({
67
- timestamp: new Date().toISOString(),
68
- ...info
69
- });
70
- if (this.buffer.length >= this.bufferSize) {
71
- this.flush();
72
- }
73
- callback();
74
- }
75
- }
76
- module.exports = {
77
- GCSTransport
78
- };
package/dist/logger/s3.js DELETED
@@ -1,78 +0,0 @@
1
- "use strict";
2
-
3
- const winston = require('winston');
4
- const {
5
- S3Client,
6
- PutObjectCommand
7
- } = require('@aws-sdk/client-s3');
8
-
9
- // Create S3 client
10
- const s3Client = new S3Client({
11
- region: process.env.AWS_REGION,
12
- credentials: {
13
- accessKeyId: process.env.AWS_ACCESS_KEY_ID,
14
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
15
- }
16
- });
17
-
18
- /**
19
- * Custom Winston transport for logging to S3
20
- */
21
- class S3Transport extends winston.Transport {
22
- constructor(opts) {
23
- super(opts);
24
- this.bucket = opts.bucket;
25
- this.prefix = opts.prefix || '';
26
- this.buffer = [];
27
- this.bufferSize = opts.bufferSize || 100;
28
- this.flushInterval = opts.flushInterval || 5000;
29
- this.setupFlushInterval();
30
- }
31
- setupFlushInterval() {
32
- setInterval(() => {
33
- this.flush();
34
- }, this.flushInterval);
35
- }
36
- async flush() {
37
- if (this.buffer.length === 0) return;
38
- const logs = this.buffer.splice(0, this.buffer.length);
39
- const date = new Date().toISOString().split('T')[0];
40
- const key = `${this.prefix}/${date}/${Date.now()}.json`;
41
- try {
42
- const command = new PutObjectCommand({
43
- Bucket: this.bucket,
44
- Key: key,
45
- Body: JSON.stringify(logs),
46
- ContentType: 'application/json'
47
- });
48
- await s3Client.send(command);
49
- } catch (error) {
50
- console.error('Failed to write logs to S3:', {
51
- error: error.message,
52
- code: error.code,
53
- bucket: this.bucket,
54
- key: key,
55
- region: process.env.AWS_REGION,
56
- stack: error.stack
57
- });
58
- // Put the logs back in the buffer
59
- this.buffer.unshift(...logs);
60
- }
61
- }
62
- log(info, callback) {
63
- setImmediate(() => {
64
- this.emit('logged', info);
65
- });
66
- this.buffer.push({
67
- timestamp: new Date().toISOString(),
68
- ...info
69
- });
70
- if (this.buffer.length >= this.bufferSize) {
71
- this.flush();
72
- }
73
- callback();
74
- }
75
- }
76
- module.exports = {
77
- S3Transport
78
- };
@@ -1,57 +0,0 @@
1
- "use strict";
2
-
3
- const {
4
- getDb
5
- } = require("../services/mongo");
6
- const {
7
- ObjectId
8
- } = require("mongodb");
9
-
10
- /**
11
- * Emit an activity to PowrBase. Inserts one document into the activities collection.
12
- * Does not send notification; an Atlas trigger on the collection handles that.
13
- *
14
- * @param {Object} options
15
- * @param {string} options.projectId - Project context (required)
16
- * @param {string} options.action - e.g. "task_created", "task_assigned", "enquiry_converted"
17
- * @param {string|ObjectId} options.userId - Who performed the action
18
- * @param {string} [options.targetType] - e.g. "task", "project", "enquiry"
19
- * @param {string|ObjectId} [options.targetId] - ID of the target entity
20
- * @param {Object} [options.metadata] - Extra payload (e.g. recipientIds, title, body, url for notification)
21
- * @returns {Promise<{ insertedId: ObjectId }>} The inserted document's _id
22
- */
23
- async function emitActivity(options) {
24
- const {
25
- projectId,
26
- action,
27
- userId,
28
- targetType,
29
- targetId,
30
- metadata = {}
31
- } = options;
32
- if (!projectId || !action) {
33
- throw new Error("emitActivity: projectId and action are required");
34
- }
35
- const normalizeId = id => {
36
- if (id == null) return null;
37
- if (typeof id === "string" && ObjectId.isValid(id)) return new ObjectId(id);
38
- return id;
39
- };
40
- const doc = {
41
- projectId,
42
- action,
43
- userId: normalizeId(userId),
44
- targetType: targetType || null,
45
- targetId: normalizeId(targetId),
46
- metadata,
47
- createdAt: new Date()
48
- };
49
- const db = await getDb();
50
- const result = await db.collection("activities").insertOne(doc);
51
- return {
52
- insertedId: result.insertedId
53
- };
54
- }
55
- module.exports = {
56
- emitActivity
57
- };
@@ -1,76 +0,0 @@
1
- "use strict";
2
-
3
- const jwt = require("jsonwebtoken");
4
- const generateToken = user => {
5
- return jwt.sign({
6
- userId: user.userId
7
- }, process.env.JWT_SECRET, {
8
- expiresIn: process.env.JWT_EXPIRES_IN || '24h'
9
- });
10
- };
11
- const validateToken = async (req, res, next) => {
12
- try {
13
- let token;
14
-
15
- // Check if token exists in headers
16
- if (req.headers.authorization && req.headers.authorization.startsWith("Bearer")) {
17
- token = req.headers.authorization.split(" ")[1];
18
- }
19
- if (!token) {
20
- return res.error("Not authorized to access this route", 401);
21
- }
22
- try {
23
- // Verify token
24
- const decoded = jwt.verify(token, process.env.JWT_SECRET);
25
-
26
- // // Get user from token
27
- // const user = await User.findByPk(decoded.userId);
28
- // if (!user) {
29
- // return res.error('User not found', 401);
30
- // }
31
-
32
- // // Add user and userId to request object
33
- // req.user = user;
34
- req.userId = decoded.userId;
35
- next();
36
- } catch (error) {
37
- return res.error("Not authorized to access this route", 401, error);
38
- }
39
- } catch (error) {
40
- return res.error("Error authenticating user", 500, error);
41
- }
42
- };
43
- const validateAuth = (options = {}) => {
44
- const {
45
- publicPaths = ["/auth", "/", "/status", "/swagger"],
46
- publicMethods = ["OPTIONS"]
47
- } = options;
48
- return async (req, res, next) => {
49
- // Skip auth validation for public paths
50
- if (publicPaths.some(path => req.path.startsWith(path))) {
51
- return next();
52
- }
53
-
54
- // Skip auth validation for public methods
55
- if (publicMethods.includes(req.method)) {
56
- return next();
57
- }
58
-
59
- // Use the original validateToken middleware for protected routes
60
- return validateToken(req, res, next);
61
- };
62
- };
63
-
64
- // exports.authorize = (...roles) => {
65
- // return (req, res, next) => {
66
- // if (!roles.includes(req.user.role)) {
67
- // return res.error(`User role ${req.user.role} is not authorized to access this route`, 403);
68
- // }
69
- // next();
70
- // };
71
- // };
72
-
73
- module.exports = {
74
- validateAuth,
75
- generateToken
76
- };
@@ -1,45 +0,0 @@
1
- "use strict";
2
-
3
- /**
4
- * Logging middleware for Express applications
5
- */
6
-
7
- const logger = (req, res, next) => {
8
- const startTime = Date.now();
9
-
10
- // Log request with redacted sensitive information
11
- try {
12
- const logBody = {
13
- ...req.body
14
- };
15
- if (logBody.password) logBody.password = "[REDACTED]";
16
- if (logBody.token) logBody.token = "[REDACTED]";
17
- console.log('Request:', {
18
- timestamp: new Date().toISOString(),
19
- method: req.method,
20
- url: req.originalUrl,
21
- ip: req.ip,
22
- userAgent: req.get('user-agent'),
23
- body: logBody,
24
- query: req.query
25
- });
26
- } catch (error) {
27
- console.error("Error logging request:", error);
28
- }
29
-
30
- // Log response with timing information
31
- res.on('finish', () => {
32
- const responseTime = Date.now() - startTime;
33
- console.log('Response:', {
34
- timestamp: new Date().toISOString(),
35
- method: req.method,
36
- url: req.originalUrl,
37
- statusCode: res.statusCode,
38
- responseTime: `${responseTime}ms`,
39
- userAgent: req.get('user-agent'),
40
- ip: req.ip
41
- });
42
- });
43
- next();
44
- };
45
- module.exports = logger;
@@ -1,53 +0,0 @@
1
- "use strict";
2
-
3
- const logger = require('../services/logger');
4
- const responseFormatter = (req, res, next) => {
5
- // Add success method to response object
6
- res.success = function (message = 'Success', statusCode = 200, data = null) {
7
- const response = {
8
- success: true,
9
- message
10
- };
11
-
12
- // Only include data if it's not null
13
- if (data !== null) {
14
- response.data = data;
15
- }
16
- return this.status(statusCode).json(response);
17
- };
18
-
19
- // Add error method to response object
20
- res.error = function (message = 'Error', statusCode = 500, error = null) {
21
- const response = {
22
- success: false,
23
- message
24
- };
25
-
26
- // Only include details if they're not null
27
- if (error !== null) {
28
- response.error = error;
29
- }
30
- return this.status(statusCode).json(response);
31
- };
32
- next();
33
- };
34
- const responseLogger = (req, res, next) => {
35
- const startTime = Date.now();
36
- res.on('finish', () => {
37
- const responseTime = Date.now() - startTime;
38
- logger.info('API Response', {
39
- method: req.method,
40
- url: req.originalUrl,
41
- statusCode: res.statusCode,
42
- responseTime: `${responseTime}ms`,
43
- userAgent: req.get('user-agent'),
44
- ip: req.ip,
45
- response: res.locals.responseBody || '[not captured]'
46
- });
47
- });
48
- next();
49
- };
50
- module.exports = {
51
- responseFormatter,
52
- responseLogger
53
- };
@@ -1,35 +0,0 @@
1
- "use strict";
2
-
3
- const winston = require('winston');
4
- const {
5
- S3Transport
6
- } = require('../utils/s3');
7
-
8
- // Custom format for logs
9
- const logFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({
10
- stack: true
11
- }), winston.format.json());
12
-
13
- // Create base transports array with console transport
14
- const transports = [new winston.transports.Console()];
15
-
16
- // Add S3 transport only in production
17
- if (process.env.NODE_ENV === 'production') {
18
- transports.push(new S3Transport({
19
- bucket: process.env.AWS_LOG_BUCKET_NAME,
20
- prefix: 'logs',
21
- bufferSize: 100,
22
- flushInterval: 5000
23
- }));
24
- }
25
-
26
- // Create the logger
27
- const logger = winston.createLogger({
28
- level: process.env.LOG_LEVEL || 'info',
29
- format: logFormat,
30
- defaultMeta: {
31
- service: process.env.LOG_SERVICE_NAME
32
- },
33
- transports
34
- });
35
- module.exports = logger;
@@ -1,19 +0,0 @@
1
- "use strict";
2
-
3
- const jwt = require('jsonwebtoken');
4
-
5
- /**
6
- * Generates a JWT token for a user
7
- * @param {Object} user - The user object
8
- * @returns {string} - The generated JWT token
9
- */
10
- const generateToken = user => {
11
- return jwt.sign({
12
- userId: user.userId
13
- }, process.env.JWT_SECRET, {
14
- expiresIn: process.env.JWT_EXPIRES_IN || '24h'
15
- });
16
- };
17
- module.exports = {
18
- generateToken
19
- };
@@ -1,57 +0,0 @@
1
- "use strict";
2
-
3
- /**
4
- * Logger utility for Express applications
5
- */
6
-
7
- const winston = require('winston');
8
- const S3Transport = require('./s3-transport');
9
-
10
- /**
11
- * Creates a Winston logger instance with configured transports
12
- * @param {Object} options - Logger configuration options
13
- * @param {string} options.service - Service name for logging
14
- * @param {string} options.level - Log level (default: 'info')
15
- * @param {Object} options.s3 - S3 transport configuration
16
- * @param {string} options.s3.bucket - S3 bucket name
17
- * @param {string} options.s3.prefix - S3 key prefix
18
- * @param {number} options.s3.bufferSize - Buffer size for S3 uploads
19
- * @param {number} options.s3.flushInterval - Flush interval in milliseconds
20
- * @returns {winston.Logger} Configured Winston logger instance
21
- */
22
- const createLogger = (options = {}) => {
23
- const {
24
- service = 'api',
25
- level = process.env.LOG_LEVEL || 'info',
26
- s3 = null
27
- } = options;
28
-
29
- // Custom format for logs
30
- const logFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({
31
- stack: true
32
- }), winston.format.json());
33
-
34
- // Create base transports array with console transport
35
- const transports = [new winston.transports.Console()];
36
-
37
- // Add S3 transport if configured
38
- if (s3 && process.env.NODE_ENV === 'production') {
39
- transports.push(new S3Transport({
40
- bucket: s3.bucket,
41
- prefix: s3.prefix || 'logs',
42
- bufferSize: s3.bufferSize || 100,
43
- flushInterval: s3.flushInterval || 5000
44
- }));
45
- }
46
-
47
- // Create and return the logger
48
- return winston.createLogger({
49
- level,
50
- format: logFormat,
51
- defaultMeta: {
52
- service
53
- },
54
- transports
55
- });
56
- };
57
- module.exports = createLogger;
@@ -1,61 +0,0 @@
1
- "use strict";
2
-
3
- /**
4
- * S3 Transport for Winston logger
5
- */
6
-
7
- const winston = require('winston');
8
- const {
9
- S3Client,
10
- PutObjectCommand
11
- } = require('@aws-sdk/client-s3');
12
-
13
- /**
14
- * Custom Winston transport for logging to S3
15
- */
16
- class S3Transport extends winston.Transport {
17
- constructor(opts) {
18
- super(opts);
19
- this.bucket = opts.bucket;
20
- this.prefix = opts.prefix || '';
21
- this.buffer = [];
22
- this.bufferSize = opts.bufferSize || 100;
23
- this.flushInterval = opts.flushInterval || 5000;
24
- this.setupFlushInterval();
25
- }
26
- setupFlushInterval() {
27
- setInterval(() => {
28
- this.flush();
29
- }, this.flushInterval);
30
- }
31
- async flush() {
32
- if (this.buffer.length === 0) return;
33
- const logs = this.buffer.splice(0, this.buffer.length);
34
- const date = new Date().toISOString().split('T')[0];
35
- const key = `${this.prefix}/${date}/${Date.now()}.json`;
36
- try {
37
- const command = new PutObjectCommand({
38
- Bucket: this.bucket,
39
- Key: key,
40
- Body: JSON.stringify(logs),
41
- ContentType: 'application/json'
42
- });
43
- await this.s3Client.send(command);
44
- } catch (error) {
45
- console.error('Error flushing logs to S3:', error);
46
- // Put logs back in buffer
47
- this.buffer.unshift(...logs);
48
- }
49
- }
50
- log(info, callback) {
51
- setImmediate(() => {
52
- this.emit('logged', info);
53
- });
54
- this.buffer.push(info);
55
- if (this.buffer.length >= this.bufferSize) {
56
- this.flush();
57
- }
58
- callback();
59
- }
60
- }
61
- module.exports = S3Transport;
package/dist/utils/s3.js DELETED
@@ -1,78 +0,0 @@
1
- "use strict";
2
-
3
- const winston = require('winston');
4
- const {
5
- S3Client,
6
- PutObjectCommand
7
- } = require('@aws-sdk/client-s3');
8
-
9
- // Create S3 client
10
- const s3Client = new S3Client({
11
- region: process.env.AWS_REGION,
12
- credentials: {
13
- accessKeyId: process.env.AWS_ACCESS_KEY_ID,
14
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
15
- }
16
- });
17
-
18
- /**
19
- * Custom Winston transport for logging to S3
20
- */
21
- class S3Transport extends winston.Transport {
22
- constructor(opts) {
23
- super(opts);
24
- this.bucket = opts.bucket;
25
- this.prefix = opts.prefix || '';
26
- this.buffer = [];
27
- this.bufferSize = opts.bufferSize || 100;
28
- this.flushInterval = opts.flushInterval || 5000;
29
- this.setupFlushInterval();
30
- }
31
- setupFlushInterval() {
32
- setInterval(() => {
33
- this.flush();
34
- }, this.flushInterval);
35
- }
36
- async flush() {
37
- if (this.buffer.length === 0) return;
38
- const logs = this.buffer.splice(0, this.buffer.length);
39
- const date = new Date().toISOString().split('T')[0];
40
- const key = `${this.prefix}/${date}/${Date.now()}.json`;
41
- try {
42
- const command = new PutObjectCommand({
43
- Bucket: this.bucket,
44
- Key: key,
45
- Body: JSON.stringify(logs),
46
- ContentType: 'application/json'
47
- });
48
- await s3Client.send(command);
49
- } catch (error) {
50
- console.error('Failed to write logs to S3:', {
51
- error: error.message,
52
- code: error.code,
53
- bucket: this.bucket,
54
- key: key,
55
- region: process.env.AWS_REGION,
56
- stack: error.stack
57
- });
58
- // Put the logs back in the buffer
59
- this.buffer.unshift(...logs);
60
- }
61
- }
62
- log(info, callback) {
63
- setImmediate(() => {
64
- this.emit('logged', info);
65
- });
66
- this.buffer.push({
67
- timestamp: new Date().toISOString(),
68
- ...info
69
- });
70
- if (this.buffer.length >= this.bufferSize) {
71
- this.flush();
72
- }
73
- callback();
74
- }
75
- }
76
- module.exports = {
77
- S3Transport
78
- };