powr-sdk-api 4.5.8 → 4.7.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/dist/config.js +0 -10
- package/dist/logger/gcs.js +78 -0
- package/dist/logger/s3.js +78 -0
- package/dist/middleware/auth.js +76 -0
- package/dist/middleware/jwtToken.js +15 -2
- package/dist/middleware/logger.js +45 -0
- package/dist/middleware/response.js +53 -0
- package/dist/routes/auth.js +73 -2
- package/dist/routes/chat.js +91 -22
- package/dist/services/logger.js +35 -0
- package/dist/services/uploadToGCS.js +0 -9
- package/dist/utils/auth.js +19 -0
- package/dist/utils/logger.js +57 -0
- package/dist/utils/s3-transport.js +61 -0
- package/dist/utils/s3.js +78 -0
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -20,16 +20,6 @@ const credentials = {
|
|
|
20
20
|
auth_provider_x509_cert_url: process.env.GOOGLE_CLOUD_AUTH_PROVIDER_X509_CERT_URL,
|
|
21
21
|
client_x509_cert_url: process.env.GOOGLE_CLOUD_CLIENT_X509_CERT_URL
|
|
22
22
|
};
|
|
23
|
-
console.log('Configuration loaded............:', {
|
|
24
|
-
mongoUri: config.mongoUri ? '***REDACTED***' : 'NOT SET',
|
|
25
|
-
jwtToken: config.jwtToken ? '***REDACTED***' : 'NOT SET',
|
|
26
|
-
projectId: config.projectId,
|
|
27
|
-
storageBucket: config.storageBucket,
|
|
28
|
-
credentials: credentials ? {
|
|
29
|
-
client_email: credentials.client_email,
|
|
30
|
-
private_key_length: credentials.private_key ? credentials.private_key.length : 0
|
|
31
|
-
} : null
|
|
32
|
-
});
|
|
33
23
|
module.exports = {
|
|
34
24
|
config,
|
|
35
25
|
credentials
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
};
|
|
@@ -10,9 +10,20 @@ const generateJWTToken = (user, profile = null) => {
|
|
|
10
10
|
fullName: user.fullName,
|
|
11
11
|
access: profile === null || profile === void 0 ? void 0 : profile.access
|
|
12
12
|
}, config.jwtToken, {
|
|
13
|
-
expiresIn: '
|
|
13
|
+
expiresIn: '7d'
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
|
+
const generateRefreshToken = user => {
|
|
17
|
+
return jwt.sign({
|
|
18
|
+
userId: user._id,
|
|
19
|
+
type: 'refresh'
|
|
20
|
+
}, config.jwtToken, {
|
|
21
|
+
expiresIn: '30d'
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const verifyRefreshToken = token => {
|
|
25
|
+
return jwt.verify(token, config.jwtToken);
|
|
26
|
+
};
|
|
16
27
|
|
|
17
28
|
// Verify JWT token middleware for APIs
|
|
18
29
|
const verifyToken = async (req, res, next) => {
|
|
@@ -43,7 +54,7 @@ const verifyToken = async (req, res, next) => {
|
|
|
43
54
|
fullName: decoded.fullName,
|
|
44
55
|
access: decoded.access
|
|
45
56
|
};
|
|
46
|
-
next();
|
|
57
|
+
return next();
|
|
47
58
|
} catch (error) {
|
|
48
59
|
console.error("Error in auth middleware:", error);
|
|
49
60
|
return res.status(401).json({
|
|
@@ -54,5 +65,7 @@ const verifyToken = async (req, res, next) => {
|
|
|
54
65
|
};
|
|
55
66
|
module.exports = {
|
|
56
67
|
generateJWTToken,
|
|
68
|
+
generateRefreshToken,
|
|
69
|
+
verifyRefreshToken,
|
|
57
70
|
verifyToken
|
|
58
71
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
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;
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
};
|
package/dist/routes/auth.js
CHANGED
|
@@ -10,7 +10,9 @@ const {
|
|
|
10
10
|
} = require("mongodb");
|
|
11
11
|
const bcrypt = require("bcrypt");
|
|
12
12
|
const {
|
|
13
|
-
generateJWTToken
|
|
13
|
+
generateJWTToken,
|
|
14
|
+
generateRefreshToken,
|
|
15
|
+
verifyRefreshToken
|
|
14
16
|
} = require("../middleware/jwtToken");
|
|
15
17
|
const {
|
|
16
18
|
config
|
|
@@ -178,6 +180,14 @@ router.post("/login", async (req, res) => {
|
|
|
178
180
|
}
|
|
179
181
|
}
|
|
180
182
|
const token = generateJWTToken(user, profile);
|
|
183
|
+
const refreshToken = generateRefreshToken(user);
|
|
184
|
+
await db.collection("users").updateOne({
|
|
185
|
+
_id: user._id
|
|
186
|
+
}, {
|
|
187
|
+
$set: {
|
|
188
|
+
refreshToken
|
|
189
|
+
}
|
|
190
|
+
});
|
|
181
191
|
const {
|
|
182
192
|
password: _,
|
|
183
193
|
_id,
|
|
@@ -205,7 +215,8 @@ router.post("/login", async (req, res) => {
|
|
|
205
215
|
success: true,
|
|
206
216
|
message: "Login successful",
|
|
207
217
|
user: mergedUser,
|
|
208
|
-
token
|
|
218
|
+
token,
|
|
219
|
+
refreshToken
|
|
209
220
|
});
|
|
210
221
|
} catch (error) {
|
|
211
222
|
console.error("Error logging in:", error);
|
|
@@ -215,6 +226,66 @@ router.post("/login", async (req, res) => {
|
|
|
215
226
|
});
|
|
216
227
|
}
|
|
217
228
|
});
|
|
229
|
+
router.post("/refresh-token", async (req, res) => {
|
|
230
|
+
const {
|
|
231
|
+
refreshToken
|
|
232
|
+
} = req.body;
|
|
233
|
+
const projectId = req.projectId;
|
|
234
|
+
try {
|
|
235
|
+
var _decoded, _decoded2;
|
|
236
|
+
if (!refreshToken) {
|
|
237
|
+
return res.status(400).json({
|
|
238
|
+
success: false,
|
|
239
|
+
message: "Refresh token is required"
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
let decoded;
|
|
243
|
+
try {
|
|
244
|
+
decoded = verifyRefreshToken(refreshToken);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return res.status(401).json({
|
|
247
|
+
success: false,
|
|
248
|
+
message: "Invalid or expired refresh token"
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (!((_decoded = decoded) !== null && _decoded !== void 0 && _decoded.userId) || ((_decoded2 = decoded) === null || _decoded2 === void 0 ? void 0 : _decoded2.type) !== "refresh") {
|
|
252
|
+
return res.status(401).json({
|
|
253
|
+
success: false,
|
|
254
|
+
message: "Invalid refresh token payload"
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const db = await getDb();
|
|
258
|
+
const user = await db.collection("users").findOne({
|
|
259
|
+
_id: new ObjectId(decoded.userId),
|
|
260
|
+
refreshToken
|
|
261
|
+
});
|
|
262
|
+
if (!user) {
|
|
263
|
+
return res.status(401).json({
|
|
264
|
+
success: false,
|
|
265
|
+
message: "Refresh token is no longer valid"
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
let profile = null;
|
|
269
|
+
if (projectId) {
|
|
270
|
+
profile = await db.collection("profiles").findOne({
|
|
271
|
+
userId: user._id,
|
|
272
|
+
projectId: projectId
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const token = generateJWTToken(user, profile);
|
|
276
|
+
return res.status(200).json({
|
|
277
|
+
success: true,
|
|
278
|
+
message: "Token refreshed successfully",
|
|
279
|
+
token
|
|
280
|
+
});
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error("Error refreshing token:", error);
|
|
283
|
+
return res.status(500).json({
|
|
284
|
+
success: false,
|
|
285
|
+
message: "Failed to refresh token."
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
});
|
|
218
289
|
|
|
219
290
|
// Forgot Password
|
|
220
291
|
router.post("/forgot-password", async (req, res) => {
|
package/dist/routes/chat.js
CHANGED
|
@@ -22,20 +22,9 @@ router.get('/conversations', async (req, res) => {
|
|
|
22
22
|
projectId: projectId
|
|
23
23
|
}).toArray();
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// Build response for each conversation
|
|
26
26
|
const conversationsWithUsers = await Promise.all(conversations.map(async conv => {
|
|
27
|
-
const
|
|
28
|
-
console.log('Looking for other user with ID:', otherUserId, 'type:', typeof otherUserId);
|
|
29
|
-
const otherUser = await db.collection('users').findOne({
|
|
30
|
-
_id: new ObjectId(otherUserId)
|
|
31
|
-
}, {
|
|
32
|
-
projection: {
|
|
33
|
-
fullName: 1,
|
|
34
|
-
username: 1,
|
|
35
|
-
avatar: 1
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
console.log('Found other user:', otherUser);
|
|
27
|
+
const isGroup = conv.type === 'group';
|
|
39
28
|
|
|
40
29
|
// Get last message
|
|
41
30
|
const lastMessage = await db.collection('messages').findOne({
|
|
@@ -54,14 +43,32 @@ router.get('/conversations', async (req, res) => {
|
|
|
54
43
|
},
|
|
55
44
|
read: false
|
|
56
45
|
});
|
|
46
|
+
let displayName, avatar;
|
|
47
|
+
if (isGroup) {
|
|
48
|
+
displayName = conv.name || 'Group';
|
|
49
|
+
avatar = null; // Groups typically use icon, not user avatar
|
|
50
|
+
} else {
|
|
51
|
+
const otherUserId = conv.participants.find(p => String(p) !== String(userId));
|
|
52
|
+
const otherUser = otherUserId ? await db.collection('users').findOne({
|
|
53
|
+
_id: new ObjectId(otherUserId)
|
|
54
|
+
}, {
|
|
55
|
+
projection: {
|
|
56
|
+
fullName: 1,
|
|
57
|
+
avatar: 1
|
|
58
|
+
}
|
|
59
|
+
}) : null;
|
|
60
|
+
displayName = (otherUser === null || otherUser === void 0 ? void 0 : otherUser.fullName) || 'Unknown User';
|
|
61
|
+
avatar = otherUser === null || otherUser === void 0 ? void 0 : otherUser.avatar;
|
|
62
|
+
}
|
|
57
63
|
return {
|
|
58
64
|
id: conv._id.toString(),
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
type: isGroup ? 'group' : 'dm',
|
|
66
|
+
name: displayName,
|
|
67
|
+
avatar,
|
|
61
68
|
lastMessage: (lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.content) || '',
|
|
62
69
|
lastMessageTime: (lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.createdAt) || conv.createdAt,
|
|
63
70
|
unreadCount,
|
|
64
|
-
isOnline: false
|
|
71
|
+
isOnline: false
|
|
65
72
|
};
|
|
66
73
|
}));
|
|
67
74
|
return res.json({
|
|
@@ -214,23 +221,84 @@ router.post('/conversations/:conversationId/messages', async (req, res) => {
|
|
|
214
221
|
}
|
|
215
222
|
});
|
|
216
223
|
|
|
217
|
-
// Create a new conversation
|
|
224
|
+
// Create a new conversation (DM or group)
|
|
218
225
|
router.post('/conversations', async (req, res) => {
|
|
219
226
|
try {
|
|
220
227
|
const {
|
|
221
228
|
participantId,
|
|
229
|
+
participantIds,
|
|
230
|
+
type,
|
|
231
|
+
name,
|
|
222
232
|
projectId: bodyProjectId
|
|
223
233
|
} = req.body;
|
|
224
234
|
const userId = req.user.powrId;
|
|
225
235
|
const userAccess = req.user.access;
|
|
226
236
|
const projectId = bodyProjectId || req.projectId;
|
|
237
|
+
const db = await getDb();
|
|
238
|
+
|
|
239
|
+
// --- GROUP ---
|
|
240
|
+
if (type === 'group') {
|
|
241
|
+
if (!participantIds || !Array.isArray(participantIds) || participantIds.length === 0) {
|
|
242
|
+
return res.status(400).json({
|
|
243
|
+
success: false,
|
|
244
|
+
message: "participantIds (array) is required for group"
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
248
|
+
return res.status(400).json({
|
|
249
|
+
success: false,
|
|
250
|
+
message: "name is required for group"
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Normalize participant IDs (ensure strings, dedupe, include creator)
|
|
255
|
+
const normalizedIds = [...new Set([userId, ...participantIds.map(id => String(id))])];
|
|
256
|
+
|
|
257
|
+
// Check if group with same projectId + name already exists
|
|
258
|
+
const existingGroup = await db.collection('conversations').findOne({
|
|
259
|
+
type: 'group',
|
|
260
|
+
projectId: projectId,
|
|
261
|
+
name: name.trim()
|
|
262
|
+
});
|
|
263
|
+
if (existingGroup) {
|
|
264
|
+
// Return existing if creator is a participant
|
|
265
|
+
const isParticipant = existingGroup.participants.some(p => String(p) === String(userId));
|
|
266
|
+
if (isParticipant) {
|
|
267
|
+
return res.json({
|
|
268
|
+
success: true,
|
|
269
|
+
data: {
|
|
270
|
+
id: existingGroup._id.toString(),
|
|
271
|
+
message: 'Group already exists'
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const conversation = {
|
|
277
|
+
type: 'group',
|
|
278
|
+
participants: normalizedIds,
|
|
279
|
+
name: name.trim(),
|
|
280
|
+
projectId: projectId,
|
|
281
|
+
createdBy: userId,
|
|
282
|
+
createdAt: new Date(),
|
|
283
|
+
updatedAt: new Date()
|
|
284
|
+
};
|
|
285
|
+
const result = await db.collection('conversations').insertOne(conversation);
|
|
286
|
+
return res.status(201).json({
|
|
287
|
+
success: true,
|
|
288
|
+
data: {
|
|
289
|
+
id: result.insertedId.toString(),
|
|
290
|
+
message: 'Group created successfully'
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- DM (default) ---
|
|
227
296
|
if (!participantId) {
|
|
228
297
|
return res.status(400).json({
|
|
229
298
|
success: false,
|
|
230
|
-
message: "Participant ID is required"
|
|
299
|
+
message: "Participant ID is required for DM (or use type: 'group' with participantIds)"
|
|
231
300
|
});
|
|
232
301
|
}
|
|
233
|
-
const db = await getDb();
|
|
234
302
|
|
|
235
303
|
// Check if user has permission to chat with this participant
|
|
236
304
|
const participant = await db.collection('users').findOne({
|
|
@@ -250,13 +318,10 @@ router.post('/conversations', async (req, res) => {
|
|
|
250
318
|
// Validate access permissions
|
|
251
319
|
let hasPermission = false;
|
|
252
320
|
if (userAccess === 100) {
|
|
253
|
-
// Admin can chat with everyone
|
|
254
321
|
hasPermission = true;
|
|
255
322
|
} else if (userAccess === 900) {
|
|
256
|
-
// Client can only chat with admins
|
|
257
323
|
hasPermission = participant.access === 100;
|
|
258
324
|
} else {
|
|
259
|
-
// Employee can chat with employees and admins
|
|
260
325
|
hasPermission = participant.access === 100 || participant.access === null;
|
|
261
326
|
}
|
|
262
327
|
if (!hasPermission) {
|
|
@@ -268,6 +333,9 @@ router.post('/conversations', async (req, res) => {
|
|
|
268
333
|
|
|
269
334
|
// Check if conversation already exists
|
|
270
335
|
const existingConversation = await db.collection('conversations').findOne({
|
|
336
|
+
type: {
|
|
337
|
+
$ne: 'group'
|
|
338
|
+
},
|
|
271
339
|
participants: {
|
|
272
340
|
$all: [userId, participantId]
|
|
273
341
|
},
|
|
@@ -283,6 +351,7 @@ router.post('/conversations', async (req, res) => {
|
|
|
283
351
|
});
|
|
284
352
|
}
|
|
285
353
|
const conversation = {
|
|
354
|
+
type: 'dm',
|
|
286
355
|
participants: [userId, participantId],
|
|
287
356
|
projectId: projectId,
|
|
288
357
|
createdAt: new Date(),
|
|
@@ -0,0 +1,35 @@
|
|
|
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;
|
|
@@ -14,15 +14,6 @@ const storage = new Storage({
|
|
|
14
14
|
projectId: config.projectId,
|
|
15
15
|
credentials: credentials
|
|
16
16
|
});
|
|
17
|
-
const safeCredentials = credentials ? {
|
|
18
|
-
client_email: credentials.client_email,
|
|
19
|
-
private_key_length: credentials.private_key ? credentials.private_key.length : 0
|
|
20
|
-
} : null;
|
|
21
|
-
console.log('GCS Storage created:', {
|
|
22
|
-
projectId: config.projectId,
|
|
23
|
-
credentials: safeCredentials,
|
|
24
|
-
storageInstanceType: typeof storage
|
|
25
|
-
});
|
|
26
17
|
const bucketName = config.storageBucket;
|
|
27
18
|
const bucket = storage.bucket(bucketName);
|
|
28
19
|
console.log('Bucket:', bucketName);
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
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;
|
|
@@ -0,0 +1,61 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "powr-sdk-api",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
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
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|