feat: Nambahin feature notifikasi
This commit is contained in:
Generated
+175
-3
@@ -27,6 +27,8 @@
|
|||||||
"nodemailer": "^7.0.3",
|
"nodemailer": "^7.0.3",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
"qs": "^6.14.0",
|
"qs": "^6.14.0",
|
||||||
|
"socket.io": "^4.8.3",
|
||||||
|
"socket.io-client": "^4.8.3",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
@@ -286,6 +288,12 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
@@ -311,6 +319,15 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cors": {
|
||||||
|
"version": "2.8.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||||
|
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/express": {
|
"node_modules/@types/express": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz",
|
||||||
@@ -410,6 +427,15 @@
|
|||||||
"@types/serve-static": "*"
|
"@types/serve-static": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/abbrev": {
|
"node_modules/abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
@@ -626,6 +652,15 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64id": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^4.5.0 || >= 5.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bcrypt": {
|
"node_modules/bcrypt": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
|
||||||
@@ -1228,9 +1263,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -1427,6 +1462,58 @@
|
|||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io": {
|
||||||
|
"version": "6.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz",
|
||||||
|
"integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/node": ">=10.0.0",
|
||||||
|
"@types/ws": "^8.5.12",
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "2.0.0",
|
||||||
|
"cookie": "~0.7.2",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.4.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.21.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz",
|
||||||
|
"integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.4.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.21.0",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -3395,6 +3482,62 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io": {
|
||||||
|
"version": "4.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
|
||||||
|
"integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "~2.0.0",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.4.1",
|
||||||
|
"engine.io": "~6.6.0",
|
||||||
|
"socket.io-adapter": "~2.5.2",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter": {
|
||||||
|
"version": "2.5.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz",
|
||||||
|
"integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "~4.4.1",
|
||||||
|
"ws": "~8.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
|
||||||
|
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.4.1",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
|
||||||
|
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.4.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sqlstring": {
|
"node_modules/sqlstring": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||||
@@ -3806,12 +3949,41 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||||
|
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xmlchars": {
|
"node_modules/xmlchars": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
"nodemailer": "^7.0.3",
|
"nodemailer": "^7.0.3",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
"qs": "^6.14.0",
|
"qs": "^6.14.0",
|
||||||
|
"socket.io": "^4.8.3",
|
||||||
|
"socket.io-client": "^4.8.3",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ model Admin {
|
|||||||
professional_bio String? @db.Text
|
professional_bio String? @db.Text
|
||||||
|
|
||||||
projects Project[]
|
projects Project[]
|
||||||
|
notifications Notification[]
|
||||||
|
|
||||||
@@map("admin")
|
@@map("admin")
|
||||||
}
|
}
|
||||||
@@ -195,6 +196,7 @@ model User {
|
|||||||
project_members ProjectMember[]
|
project_members ProjectMember[]
|
||||||
task_submissions TaskSubmission[]
|
task_submissions TaskSubmission[]
|
||||||
certificates Certificate[]
|
certificates Certificate[]
|
||||||
|
notifications Notification[]
|
||||||
|
|
||||||
@@index([email])
|
@@index([email])
|
||||||
@@map("user")
|
@@map("user")
|
||||||
@@ -347,3 +349,20 @@ model Certificate {
|
|||||||
@@index([id_user], map: "certificate_id_user_foreign")
|
@@index([id_user], map: "certificate_id_user_foreign")
|
||||||
@@map("certificate")
|
@@map("certificate")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Notification {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
id_user Int? // For interns
|
||||||
|
id_admin Int? // For mentors/admins
|
||||||
|
title String @db.VarChar(255)
|
||||||
|
message String @db.Text
|
||||||
|
is_read Boolean @default(false)
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
|
||||||
|
user User? @relation(fields: [id_user], references: [id], onDelete: Cascade)
|
||||||
|
admin Admin? @relation(fields: [id_admin], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([id_user])
|
||||||
|
@@index([id_admin])
|
||||||
|
@@map("notification")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
const { Server } = require('socket.io');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const { config } = require('../infra/global_config');
|
||||||
|
|
||||||
|
let io = null;
|
||||||
|
|
||||||
|
function initSocket(server) {
|
||||||
|
io = new Server(server, {
|
||||||
|
cors: {
|
||||||
|
origin: "*",
|
||||||
|
methods: ["GET", "POST", "PATCH", "PUT", "DELETE"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
io.use((socket, next) => {
|
||||||
|
const token = socket.handshake.auth?.token || socket.handshake.query?.token;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return next(new Error('Authentication token required'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = config.jwtPublicKey ? config.jwtPublicKey.replace(/\\n/g, '\n') : null;
|
||||||
|
if (!publicKey) {
|
||||||
|
return next(new Error('JWT public key not configured'));
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.verify(token, publicKey, { algorithms: ['RS256'] }, (err, decoded) => {
|
||||||
|
if (err) {
|
||||||
|
return next(new Error('Authentication failed: Invalid or expired token'));
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.user = {
|
||||||
|
id: decoded.id,
|
||||||
|
role: decoded.role,
|
||||||
|
email: decoded.email
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
const { id, role } = socket.user;
|
||||||
|
|
||||||
|
if (role === 'admin' || role === 'mentor') {
|
||||||
|
socket.join(`admin:${id}`);
|
||||||
|
} else if (role === 'intern') {
|
||||||
|
socket.join(`user:${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
// Otomatis keluar [Rafi 14:47 23/06/2026]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return io;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIO() {
|
||||||
|
return io;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initSocket, getIO };
|
||||||
+8
-1
@@ -1,5 +1,7 @@
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const http = require('http');
|
||||||
|
const { initSocket } = require('./helpers/socket/socket_connection');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const adminRoutes = require('./routes/admin.routes');
|
const adminRoutes = require('./routes/admin.routes');
|
||||||
@@ -15,6 +17,7 @@ const batchRoutes = require('./routes/batch.routes');
|
|||||||
const projectRoutes = require('./routes/project.routes');
|
const projectRoutes = require('./routes/project.routes');
|
||||||
const taskRoutes = require('./routes/task.routes');
|
const taskRoutes = require('./routes/task.routes');
|
||||||
const certificateRoutes = require('./routes/certificate.routes');
|
const certificateRoutes = require('./routes/certificate.routes');
|
||||||
|
const notificationRoutes = require('./routes/notification.routes');
|
||||||
const setupSwaggerDocs = require('./docs/swagger');
|
const setupSwaggerDocs = require('./docs/swagger');
|
||||||
|
|
||||||
const PORT = process.env.PORT;
|
const PORT = process.env.PORT;
|
||||||
@@ -60,6 +63,7 @@ app.use("/batch-api", batchRoutes);
|
|||||||
app.use("/project-api", projectRoutes);
|
app.use("/project-api", projectRoutes);
|
||||||
app.use("/task-api", taskRoutes);
|
app.use("/task-api", taskRoutes);
|
||||||
app.use("/certificate-api", certificateRoutes);
|
app.use("/certificate-api", certificateRoutes);
|
||||||
|
app.use("/notification-api", notificationRoutes);
|
||||||
// app.use("/project-member-api", projectMemberRoutes);
|
// app.use("/project-member-api", projectMemberRoutes);
|
||||||
|
|
||||||
|
|
||||||
@@ -73,8 +77,11 @@ app.use((req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const server = http.createServer(app);
|
||||||
|
initSocket(server);
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
app.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`listening at http://localhost:${PORT}`);
|
console.log(`listening at http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
const notificationService = require('../services/notification.service');
|
||||||
|
const { response, error } = require('../../../helpers/utils/wrapper');
|
||||||
|
const { SUCCESS, ERROR } = require('../../../helpers/http-status/status_code');
|
||||||
|
const { InternalServerError } = require('../../../helpers/error');
|
||||||
|
|
||||||
|
const getActor = (req) => ({
|
||||||
|
id: req.id,
|
||||||
|
role: req.role,
|
||||||
|
email: req.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
class NotificationController {
|
||||||
|
async getNotifications(req, res) {
|
||||||
|
try {
|
||||||
|
const actor = getActor(req);
|
||||||
|
const result = await notificationService.getNotifications(actor);
|
||||||
|
|
||||||
|
if (result.err) {
|
||||||
|
return response(res, 'fail', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, 'success', result, 'Notifications retrieved successfully', SUCCESS.OK);
|
||||||
|
} catch (err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new InternalServerError(err.message)),
|
||||||
|
'Unexpected error occurred',
|
||||||
|
ERROR.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAsRead(req, res) {
|
||||||
|
try {
|
||||||
|
const actor = getActor(req);
|
||||||
|
const result = await notificationService.markAsRead(req.params.id, actor);
|
||||||
|
|
||||||
|
if (result.err) {
|
||||||
|
return response(res, 'fail', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, 'success', result, 'Notification marked as read successfully', SUCCESS.OK);
|
||||||
|
} catch (err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new InternalServerError(err.message)),
|
||||||
|
'Unexpected error occurred',
|
||||||
|
ERROR.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAllAsRead(req, res) {
|
||||||
|
try {
|
||||||
|
const actor = getActor(req);
|
||||||
|
const result = await notificationService.markAllAsRead(actor);
|
||||||
|
|
||||||
|
if (result.err) {
|
||||||
|
return response(res, 'fail', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response(res, 'success', result, 'All notifications marked as read', SUCCESS.OK);
|
||||||
|
} catch (err) {
|
||||||
|
return response(
|
||||||
|
res,
|
||||||
|
'fail',
|
||||||
|
error(new InternalServerError(err.message)),
|
||||||
|
'Unexpected error occurred',
|
||||||
|
ERROR.INTERNAL_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new NotificationController();
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const notificationController = require('./controllers/notification.controller');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
notificationController
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
const prisma = require('../../../helpers/db/db_connection');
|
||||||
|
|
||||||
|
class NotificationRepository {
|
||||||
|
async create(data) {
|
||||||
|
return prisma.notification.create({
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findManyByUser(userId) {
|
||||||
|
return prisma.notification.findMany({
|
||||||
|
where: { id_user: parseInt(userId) },
|
||||||
|
orderBy: { created_at: 'desc' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findManyByAdmin(adminId) {
|
||||||
|
return prisma.notification.findMany({
|
||||||
|
where: { id_admin: parseInt(adminId) },
|
||||||
|
orderBy: { created_at: 'desc' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id) {
|
||||||
|
return prisma.notification.findUnique({
|
||||||
|
where: { id: parseInt(id) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateReadStatus(id, isRead) {
|
||||||
|
return prisma.notification.update({
|
||||||
|
where: { id: parseInt(id) },
|
||||||
|
data: { is_read: isRead }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAllAsReadForUser(userId) {
|
||||||
|
return prisma.notification.updateMany({
|
||||||
|
where: {
|
||||||
|
id_user: parseInt(userId),
|
||||||
|
is_read: false
|
||||||
|
},
|
||||||
|
data: { is_read: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAllAsReadForAdmin(adminId) {
|
||||||
|
return prisma.notification.updateMany({
|
||||||
|
where: {
|
||||||
|
id_admin: parseInt(adminId),
|
||||||
|
is_read: false
|
||||||
|
},
|
||||||
|
data: { is_read: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new NotificationRepository();
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
const notificationRepository = require('../repositories/notification.repository');
|
||||||
|
const taskRepository = require('../../task/repositories/task.repository');
|
||||||
|
const { getIO } = require('../../../helpers/socket/socket_connection');
|
||||||
|
const { data, error } = require('../../../helpers/utils/wrapper');
|
||||||
|
const { NotFoundError, ForbiddenError, BadRequestError } = require('../../../helpers/error');
|
||||||
|
|
||||||
|
class NotificationService {
|
||||||
|
async createNotificationForIntern(userId, title, message) {
|
||||||
|
try {
|
||||||
|
const notification = await notificationRepository.create({
|
||||||
|
id_user: parseInt(userId),
|
||||||
|
id_admin: null,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
is_read: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
if (io) {
|
||||||
|
io.to(`user:${userId}`).emit('notification', notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data(notification);
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNotificationForMentor(adminId, title, message) {
|
||||||
|
try {
|
||||||
|
const notification = await notificationRepository.create({
|
||||||
|
id_user: null,
|
||||||
|
id_admin: parseInt(adminId),
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
is_read: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const io = getIO();
|
||||||
|
if (io) {
|
||||||
|
io.to(`admin:${adminId}`).emit('notification', notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data(notification);
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNotificationForProjectInterns(projectId, title, message) {
|
||||||
|
try {
|
||||||
|
const project = await taskRepository.findProjectById(projectId);
|
||||||
|
if (!project || !project.members) {
|
||||||
|
return data([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (const member of project.members) {
|
||||||
|
const res = await this.createNotificationForIntern(member.id_user, title, message);
|
||||||
|
if (res.data) {
|
||||||
|
results.push(res.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data(results);
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNotifications(actor = {}) {
|
||||||
|
try {
|
||||||
|
if (!actor?.id || !actor?.role) {
|
||||||
|
return error(new BadRequestError('Authentication data is missing'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let notifications = [];
|
||||||
|
if (actor.role === 'admin' || actor.role === 'mentor') {
|
||||||
|
notifications = await notificationRepository.findManyByAdmin(actor.id);
|
||||||
|
} else if (actor.role === 'intern') {
|
||||||
|
notifications = await notificationRepository.findManyByUser(actor.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data(notifications);
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAsRead(idNotification, actor = {}) {
|
||||||
|
try {
|
||||||
|
if (!actor?.id || !actor?.role) {
|
||||||
|
return error(new BadRequestError('Authentication data is missing'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const notification = await notificationRepository.findById(idNotification);
|
||||||
|
if (!notification) {
|
||||||
|
return error(new NotFoundError('Notification not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.role === 'intern') {
|
||||||
|
if (notification.id_user !== parseInt(actor.id)) {
|
||||||
|
return error(new ForbiddenError('Access denied: not your notification'));
|
||||||
|
}
|
||||||
|
} else if (actor.role === 'admin' || actor.role === 'mentor') {
|
||||||
|
if (notification.id_admin !== parseInt(actor.id)) {
|
||||||
|
return error(new ForbiddenError('Access denied: not your notification'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return error(new ForbiddenError('Access denied: invalid role'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await notificationRepository.updateReadStatus(idNotification, true);
|
||||||
|
return data(updated);
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAllAsRead(actor = {}) {
|
||||||
|
try {
|
||||||
|
if (!actor?.id || !actor?.role) {
|
||||||
|
return error(new BadRequestError('Authentication data is missing'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.role === 'admin' || actor.role === 'mentor') {
|
||||||
|
await notificationRepository.markAllAsReadForAdmin(actor.id);
|
||||||
|
} else if (actor.role === 'intern') {
|
||||||
|
await notificationRepository.markAllAsReadForUser(actor.id);
|
||||||
|
} else {
|
||||||
|
return error(new ForbiddenError('Access denied: invalid role'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return data({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
return error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new NotificationService();
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
const projectRepository = require("../repositories/project.repository");
|
const projectRepository = require("../repositories/project.repository");
|
||||||
|
const notificationService = require("../../notification/services/notification.service");
|
||||||
const { data, error } = require("../../../helpers/utils/wrapper");
|
const { data, error } = require("../../../helpers/utils/wrapper");
|
||||||
const {
|
const {
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
@@ -86,6 +87,14 @@ class ProjectService {
|
|||||||
userIds,
|
userIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (const idUser of userIds) {
|
||||||
|
await notificationService.createNotificationForIntern(
|
||||||
|
idUser,
|
||||||
|
"Diterima ke Proyek",
|
||||||
|
`Anda telah bergabung ke proyek '${project.project_name}'.`
|
||||||
|
).catch(err => console.error("Failed to send project assignment notification:", err));
|
||||||
|
}
|
||||||
|
|
||||||
return data(this.mapProjectDetail(project));
|
return data(this.mapProjectDetail(project));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return error(err);
|
return error(err);
|
||||||
@@ -516,6 +525,12 @@ class ProjectService {
|
|||||||
membership = await projectRepository.assignMember(id_project, id_user);
|
membership = await projectRepository.assignMember(id_project, id_user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await notificationService.createNotificationForIntern(
|
||||||
|
id_user,
|
||||||
|
"Diterima ke Proyek",
|
||||||
|
`Anda telah bergabung ke proyek '${project.project_name}'.`
|
||||||
|
).catch(err => console.error("Failed to send project assignment notification:", err));
|
||||||
|
|
||||||
return data(membership);
|
return data(membership);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return error(err);
|
return error(err);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const taskRepository = require('../repositories/task.repository');
|
const taskRepository = require('../repositories/task.repository');
|
||||||
|
const prisma = require('../../../helpers/db/db_connection');
|
||||||
|
const notificationService = require('../../notification/services/notification.service');
|
||||||
const { taskUploadDir } = require('../helpers/taskUpload.helper');
|
const { taskUploadDir } = require('../helpers/taskUpload.helper');
|
||||||
const { data, error } = require('../../../helpers/utils/wrapper');
|
const { data, error } = require('../../../helpers/utils/wrapper');
|
||||||
const {
|
const {
|
||||||
@@ -43,6 +45,12 @@ class TaskService {
|
|||||||
submission_type: payload.submission_type,
|
submission_type: payload.submission_type,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await notificationService.createNotificationForProjectInterns(
|
||||||
|
idProject,
|
||||||
|
"Tugas Baru Ditambahkan",
|
||||||
|
`Mentor telah membuat tugas baru: '${task.title}' di proyek '${project.project_name}'.`
|
||||||
|
).catch(err => console.error("Failed to send task creation notifications:", err));
|
||||||
|
|
||||||
return data(this.mapTask(task));
|
return data(this.mapTask(task));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return error(err);
|
return error(err);
|
||||||
@@ -114,7 +122,7 @@ class TaskService {
|
|||||||
return error(accessError);
|
return error(accessError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actor.role === 'admin') {
|
if (this.isMentorOrAdmin(actor)) {
|
||||||
return data(this.mapAdminTaskDetail(task));
|
return data(this.mapAdminTaskDetail(task));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +296,21 @@ class TaskService {
|
|||||||
submission = await taskRepository.createSubmission(submissionData);
|
submission = await taskRepository.createSubmission(submissionData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectDetails = await taskRepository.findProjectById(task.id_project);
|
||||||
|
if (projectDetails) {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: parseInt(actor.id) },
|
||||||
|
select: { full_name: true }
|
||||||
|
});
|
||||||
|
const internName = user?.full_name || actor.email;
|
||||||
|
|
||||||
|
await notificationService.createNotificationForMentor(
|
||||||
|
projectDetails.id_admin,
|
||||||
|
"Pengiriman Tugas Baru",
|
||||||
|
`Intern '${internName}' telah mengirimkan jawaban untuk tugas '${task.title}'.`
|
||||||
|
).catch(err => console.error("Failed to send task submission notification:", err));
|
||||||
|
}
|
||||||
|
|
||||||
return data(this.mapSubmission(submission));
|
return data(this.mapSubmission(submission));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return error(err);
|
return error(err);
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { notificationController } = require('../modules/notification');
|
||||||
|
const { verifyJWT } = require('../middleware/verifyJWT');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* Notification:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* example: 1
|
||||||
|
* id_user:
|
||||||
|
* type: integer
|
||||||
|
* nullable: true
|
||||||
|
* example: 1
|
||||||
|
* id_admin:
|
||||||
|
* type: integer
|
||||||
|
* nullable: true
|
||||||
|
* example: null
|
||||||
|
* title:
|
||||||
|
* type: string
|
||||||
|
* example: "Tugas Baru"
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: "Mentor telah menambahkan tugas baru."
|
||||||
|
* is_read:
|
||||||
|
* type: boolean
|
||||||
|
* example: false
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* example: "2026-06-09T14:00:00.000Z"
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /notification-api:
|
||||||
|
* get:
|
||||||
|
* summary: Get notifications for logged-in user
|
||||||
|
* description: Retrieve all notifications for the currently authenticated user (intern, mentor, or admin).
|
||||||
|
* tags: [Notification]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Notifications retrieved successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* data:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/Notification'
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: "Notifications retrieved successfully"
|
||||||
|
* code:
|
||||||
|
* type: integer
|
||||||
|
* example: 200
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
*/
|
||||||
|
router.get('/', verifyJWT, notificationController.getNotifications);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /notification-api/read-all:
|
||||||
|
* patch:
|
||||||
|
* summary: Mark all notifications as read
|
||||||
|
* description: Mark all notifications for the currently authenticated user as read.
|
||||||
|
* tags: [Notification]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: All notifications marked as read
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: "All notifications marked as read"
|
||||||
|
* code:
|
||||||
|
* type: integer
|
||||||
|
* example: 200
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
*/
|
||||||
|
router.patch('/read-all', verifyJWT, notificationController.markAllAsRead);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /notification-api/{id}/read:
|
||||||
|
* patch:
|
||||||
|
* summary: Mark a single notification as read
|
||||||
|
* description: Mark a specific notification as read by its ID. Users can only read their own notifications.
|
||||||
|
* tags: [Notification]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* required: true
|
||||||
|
* description: Notification ID
|
||||||
|
* example: 1
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Notification marked as read
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* data:
|
||||||
|
* $ref: '#/components/schemas/Notification'
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: "Notification marked as read"
|
||||||
|
* code:
|
||||||
|
* type: integer
|
||||||
|
* example: 200
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden (Accessing another user's notification)
|
||||||
|
* 404:
|
||||||
|
* description: Notification not found
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
*/
|
||||||
|
router.patch('/:id/read', verifyJWT, notificationController.markAsRead);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const BASE_URL = 'http://localhost:9000';
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
console.log('=== STARTING INTEGRATION TESTS FOR MENTOR TASK MANAGEMENT ===\n');
|
||||||
|
|
||||||
|
let adminToken = '';
|
||||||
|
let mentorToken = '';
|
||||||
|
let otherMentorToken = '';
|
||||||
|
let createdProjectId = null;
|
||||||
|
let otherProjectId = null;
|
||||||
|
let taskId = null;
|
||||||
|
|
||||||
|
const mentorEmail = `mentor_task_${Date.now()}@internify.com`;
|
||||||
|
const otherMentorEmail = `other_mentor_task_${Date.now()}@internify.com`;
|
||||||
|
const mentorPassword = 'Password123';
|
||||||
|
|
||||||
|
// Helper config generator
|
||||||
|
const getHeader = (token) => ({
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Login as Admin
|
||||||
|
console.log('1. Logging in as Admin (admin1@internify.com)...');
|
||||||
|
const loginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: 'admin1@internify.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
adminToken = loginRes.data.data.token;
|
||||||
|
console.log(' Admin logged in successfully!\n');
|
||||||
|
|
||||||
|
// 2. Create Mentor Accounts via Admin API
|
||||||
|
console.log(`2. Creating mentor accounts...`);
|
||||||
|
await axios.post(`${BASE_URL}/admin-api/add`, {
|
||||||
|
nama_depan: 'John',
|
||||||
|
nama_belakang: 'MentorTask',
|
||||||
|
email: mentorEmail,
|
||||||
|
password: mentorPassword,
|
||||||
|
role: 'mentor'
|
||||||
|
}, getHeader(adminToken));
|
||||||
|
|
||||||
|
await axios.post(`${BASE_URL}/admin-api/add`, {
|
||||||
|
nama_depan: 'Jane',
|
||||||
|
nama_belakang: 'OtherMentorTask',
|
||||||
|
email: otherMentorEmail,
|
||||||
|
password: mentorPassword,
|
||||||
|
role: 'mentor'
|
||||||
|
}, getHeader(adminToken));
|
||||||
|
console.log(' Mentor accounts created successfully!\n');
|
||||||
|
|
||||||
|
// 3. Login as Mentors
|
||||||
|
console.log('3. Logging in as mentors...');
|
||||||
|
const mentorLoginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: mentorEmail,
|
||||||
|
password: mentorPassword
|
||||||
|
});
|
||||||
|
mentorToken = mentorLoginRes.data.data.token;
|
||||||
|
|
||||||
|
const otherMentorLoginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: otherMentorEmail,
|
||||||
|
password: mentorPassword
|
||||||
|
});
|
||||||
|
otherMentorToken = otherMentorLoginRes.data.data.token;
|
||||||
|
console.log(' Mentors logged in successfully!\n');
|
||||||
|
|
||||||
|
// 4. Mentors create projects
|
||||||
|
console.log('4. Creating projects for mentors...');
|
||||||
|
const createProjectRes = await axios.post(`${BASE_URL}/project-api/add`, {
|
||||||
|
project_icon: 'code',
|
||||||
|
project_name: `Mentor's Project - ${Date.now()}`,
|
||||||
|
description: 'Project to test mentor task permissions.',
|
||||||
|
start_date: '2026-07-01',
|
||||||
|
end_date: '2026-10-01',
|
||||||
|
member_emails: []
|
||||||
|
}, getHeader(mentorToken));
|
||||||
|
createdProjectId = createProjectRes.data.data.id;
|
||||||
|
|
||||||
|
const otherProjectRes = await axios.post(`${BASE_URL}/project-api/add`, {
|
||||||
|
project_icon: 'shield',
|
||||||
|
project_name: `Other Mentor's Project - ${Date.now()}`,
|
||||||
|
description: 'Project owned by another mentor.',
|
||||||
|
start_date: '2026-07-01',
|
||||||
|
end_date: '2026-10-01',
|
||||||
|
member_emails: []
|
||||||
|
}, getHeader(otherMentorToken));
|
||||||
|
otherProjectId = otherProjectRes.data.data.id;
|
||||||
|
console.log(` Projects created: Mentor Project ID = ${createdProjectId}, Other Project ID = ${otherProjectId}\n`);
|
||||||
|
|
||||||
|
// 5. Mentor creates a task in their project
|
||||||
|
console.log(`5. Mentor creating task in project ID ${createdProjectId}...`);
|
||||||
|
const createTaskRes = await axios.post(`${BASE_URL}/task-api/projects/${createdProjectId}/tasks`, {
|
||||||
|
title: 'Task 1',
|
||||||
|
description: 'First task created by mentor.',
|
||||||
|
deadline_date: '2026-08-01',
|
||||||
|
specific_time: '23:59',
|
||||||
|
submission_type: 'url_link'
|
||||||
|
}, getHeader(mentorToken));
|
||||||
|
taskId = createTaskRes.data.data.id;
|
||||||
|
console.log(` Task created successfully! ID: ${taskId}\n`);
|
||||||
|
|
||||||
|
// 6. Mentor tries to create task in project they do not own
|
||||||
|
console.log(`6. Testing isolation: Mentor trying to create task in project ID ${otherProjectId} (owned by other mentor)...`);
|
||||||
|
try {
|
||||||
|
await axios.post(`${BASE_URL}/task-api/projects/${otherProjectId}/tasks`, {
|
||||||
|
title: 'Task in other project',
|
||||||
|
description: 'Should fail.',
|
||||||
|
deadline_date: '2026-08-01',
|
||||||
|
specific_time: '23:59',
|
||||||
|
submission_type: 'url_link'
|
||||||
|
}, getHeader(mentorToken));
|
||||||
|
throw new Error('TEST FAILED: Mentor created a task in a project they do not own!');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && err.response.status === 403) {
|
||||||
|
console.log(' PASSED: Access denied with 403 Forbidden as expected.\n');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Mentor retrieves task list of their project
|
||||||
|
console.log(`7. Mentor retrieving task list for project ID ${createdProjectId}...`);
|
||||||
|
const getTasksRes = await axios.get(`${BASE_URL}/task-api/projects/${createdProjectId}/tasks`, getHeader(mentorToken));
|
||||||
|
const tasks = getTasksRes.data.data;
|
||||||
|
console.log(` Tasks found: ${tasks.length}`);
|
||||||
|
if (tasks.length !== 1 || tasks[0].id !== taskId) {
|
||||||
|
throw new Error('TEST FAILED: Task list not retrieved correctly by mentor!');
|
||||||
|
}
|
||||||
|
console.log(' PASSED: Task list retrieved successfully.\n');
|
||||||
|
|
||||||
|
// 8. Mentor retrieves details of their task
|
||||||
|
console.log(`8. Mentor retrieving details of task ID ${taskId}...`);
|
||||||
|
const getTaskDetailRes = await axios.get(`${BASE_URL}/task-api/tasks/${taskId}`, getHeader(mentorToken));
|
||||||
|
const taskDetail = getTaskDetailRes.data.data;
|
||||||
|
|
||||||
|
// Mentor should get admin-like details including the submission_summary and submissions list.
|
||||||
|
// Let's verify if submission_summary exists in response.
|
||||||
|
console.log(' Retrieved detail data fields:', Object.keys(taskDetail));
|
||||||
|
if (!taskDetail.submission_summary) {
|
||||||
|
throw new Error('TEST FAILED: Mentor retrieved detail as an intern (missing submission_summary)!');
|
||||||
|
}
|
||||||
|
console.log(' PASSED: Mentor successfully retrieved detailed view of the task with submission summary.\n');
|
||||||
|
|
||||||
|
// 9. Mentor updates their task
|
||||||
|
console.log(`9. Mentor updating task ID ${taskId}...`);
|
||||||
|
await axios.patch(`${BASE_URL}/task-api/tasks/${taskId}`, {
|
||||||
|
title: 'Task 1 Updated',
|
||||||
|
description: 'Updated description by mentor.'
|
||||||
|
}, getHeader(mentorToken));
|
||||||
|
console.log(' PASSED: Task updated successfully.\n');
|
||||||
|
|
||||||
|
// 10. Mentor tries to update task they do not own
|
||||||
|
// Let's create a task in the other project first
|
||||||
|
const otherTaskRes = await axios.post(`${BASE_URL}/task-api/projects/${otherProjectId}/tasks`, {
|
||||||
|
title: 'Other Task',
|
||||||
|
description: 'Task by other mentor.',
|
||||||
|
deadline_date: '2026-08-01',
|
||||||
|
specific_time: '23:59',
|
||||||
|
submission_type: 'url_link'
|
||||||
|
}, getHeader(otherMentorToken));
|
||||||
|
const otherTaskId = otherTaskRes.data.data.id;
|
||||||
|
|
||||||
|
console.log(`10. Testing isolation: Mentor trying to update task ID ${otherTaskId}...`);
|
||||||
|
try {
|
||||||
|
await axios.patch(`${BASE_URL}/task-api/tasks/${otherTaskId}`, {
|
||||||
|
title: 'Unauthorized Update'
|
||||||
|
}, getHeader(mentorToken));
|
||||||
|
throw new Error('TEST FAILED: Mentor updated a task they do not own!');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && err.response.status === 403) {
|
||||||
|
console.log(' PASSED: Access denied with 403 Forbidden as expected.\n');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. Mentor deletes their task
|
||||||
|
console.log(`11. Mentor deleting task ID ${taskId}...`);
|
||||||
|
await axios.delete(`${BASE_URL}/task-api/tasks/${taskId}`, getHeader(mentorToken));
|
||||||
|
console.log(' PASSED: Task deleted successfully.\n');
|
||||||
|
|
||||||
|
// 12. Mentor tries to delete task they do not own
|
||||||
|
console.log(`12. Testing isolation: Mentor trying to delete task ID ${otherTaskId}...`);
|
||||||
|
try {
|
||||||
|
await axios.delete(`${BASE_URL}/task-api/tasks/${otherTaskId}`, getHeader(mentorToken));
|
||||||
|
throw new Error('TEST FAILED: Mentor deleted a task they do not own!');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && err.response.status === 403) {
|
||||||
|
console.log(' PASSED: Access denied with 403 Forbidden as expected.\n');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== ALL INTEGRATION TESTS PASSED SUCCESSFULLY! ===');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('=== TEST EXECUTION ENCOUNTERED AN ERROR ===');
|
||||||
|
if (err.response) {
|
||||||
|
console.error(`Status: ${err.response.status}`);
|
||||||
|
console.error('Response Data:', JSON.stringify(err.response.data, null, 2));
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests();
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const ioClient = require('socket.io-client');
|
||||||
|
const prisma = require('../src/helpers/db/db_connection');
|
||||||
|
|
||||||
|
const BASE_URL = 'http://localhost:9000';
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
console.log('=== STARTING INTEGRATION TESTS FOR NOTIFICATION SYSTEM ===\n');
|
||||||
|
|
||||||
|
// 0. Clean up existing memberships for rafi@student.com (user ID 1) to allow project assignment
|
||||||
|
console.log('0. Cleaning up existing project memberships for rafi@student.com...');
|
||||||
|
await prisma.projectMember.deleteMany({
|
||||||
|
where: { id_user: 1 }
|
||||||
|
});
|
||||||
|
console.log(' Cleanup successful!\n');
|
||||||
|
|
||||||
|
let adminToken = '';
|
||||||
|
let mentorToken = '';
|
||||||
|
let internToken = '';
|
||||||
|
let mentorId = null;
|
||||||
|
let internId = null;
|
||||||
|
let createdProjectId = null;
|
||||||
|
let taskId = null;
|
||||||
|
|
||||||
|
const mentorEmail = `mentor_notif_${Date.now()}@internify.com`;
|
||||||
|
const mentorPassword = 'Password123';
|
||||||
|
const internEmail = 'rafi@student.com';
|
||||||
|
const internPassword = 'password123';
|
||||||
|
|
||||||
|
// Helper config generator
|
||||||
|
const getHeader = (token) => ({
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
let internSocket = null;
|
||||||
|
let mentorSocket = null;
|
||||||
|
|
||||||
|
const internReceivedNotifs = [];
|
||||||
|
const mentorReceivedNotifs = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Login as Admin
|
||||||
|
console.log('1. Logging in as Admin (admin1@internify.com)...');
|
||||||
|
const loginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: 'admin1@internify.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
adminToken = loginRes.data.data.token;
|
||||||
|
console.log(' Admin logged in successfully!\n');
|
||||||
|
|
||||||
|
// 2. Create Mentor Account via Admin API
|
||||||
|
console.log(`2. Creating a new Mentor account (${mentorEmail})...`);
|
||||||
|
const createMentorRes = await axios.post(`${BASE_URL}/admin-api/add`, {
|
||||||
|
nama_depan: 'Jane',
|
||||||
|
nama_belakang: 'MentorNotif',
|
||||||
|
email: mentorEmail,
|
||||||
|
password: mentorPassword,
|
||||||
|
role: 'mentor'
|
||||||
|
}, getHeader(adminToken));
|
||||||
|
console.log(' Mentor account created successfully!\n');
|
||||||
|
|
||||||
|
// 3. Login as the new Mentor
|
||||||
|
console.log('3. Logging in as the new Mentor...');
|
||||||
|
const mentorLoginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: mentorEmail,
|
||||||
|
password: mentorPassword
|
||||||
|
});
|
||||||
|
mentorToken = mentorLoginRes.data.data.token;
|
||||||
|
mentorId = mentorLoginRes.data.data.user.id;
|
||||||
|
console.log(` Mentor logged in successfully! ID: ${mentorId}\n`);
|
||||||
|
|
||||||
|
// 4. Login as Intern
|
||||||
|
console.log(`4. Logging in as Intern (${internEmail})...`);
|
||||||
|
const internLoginRes = await axios.post(`${BASE_URL}/auth-api/login`, {
|
||||||
|
email: internEmail,
|
||||||
|
password: internPassword
|
||||||
|
});
|
||||||
|
internToken = internLoginRes.data.data.token;
|
||||||
|
internId = internLoginRes.data.data.user.id;
|
||||||
|
console.log(` Intern logged in successfully! ID: ${internId}\n`);
|
||||||
|
|
||||||
|
// 5. Establish WebSocket Connections for Intern and Mentor
|
||||||
|
console.log('5. Connecting to WebSocket (Socket.io) server...');
|
||||||
|
|
||||||
|
internSocket = ioClient(BASE_URL, {
|
||||||
|
auth: { token: internToken },
|
||||||
|
transports: ['websocket']
|
||||||
|
});
|
||||||
|
|
||||||
|
mentorSocket = ioClient(BASE_URL, {
|
||||||
|
auth: { token: mentorToken },
|
||||||
|
transports: ['websocket']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup socket listeners
|
||||||
|
internSocket.on('notification', (notif) => {
|
||||||
|
console.log(` [WS Event] Intern received real-time notification: "${notif.title}" - ${notif.message}`);
|
||||||
|
internReceivedNotifs.push(notif);
|
||||||
|
});
|
||||||
|
|
||||||
|
mentorSocket.on('notification', (notif) => {
|
||||||
|
console.log(` [WS Event] Mentor received real-time notification: "${notif.title}" - ${notif.message}`);
|
||||||
|
mentorReceivedNotifs.push(notif);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait a brief moment to ensure sockets are connected
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
console.log(' Sockets connected!\n');
|
||||||
|
|
||||||
|
// 6. Mentor creates project and assigns Intern
|
||||||
|
console.log(`6. Mentor creating project and assigning intern (ID: ${internId})...`);
|
||||||
|
const createProjectRes = await axios.post(`${BASE_URL}/project-api/add`, {
|
||||||
|
project_icon: 'code',
|
||||||
|
project_name: `Notification Test Project - ${Date.now()}`,
|
||||||
|
description: 'Project to test WebSocket notifications.',
|
||||||
|
start_date: '2026-07-01',
|
||||||
|
end_date: '2026-10-01',
|
||||||
|
member_emails: [internEmail]
|
||||||
|
}, getHeader(mentorToken));
|
||||||
|
createdProjectId = createProjectRes.data.data.id;
|
||||||
|
console.log(` Project created successfully! ID: ${createdProjectId}`);
|
||||||
|
|
||||||
|
// Wait for the WS notification to arrive
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Check if intern received "Diterima ke Proyek" notification
|
||||||
|
const assignmentNotif = internReceivedNotifs.find(n => n.title === 'Diterima ke Proyek');
|
||||||
|
if (!assignmentNotif) {
|
||||||
|
throw new Error('TEST FAILED: Intern did not receive real-time project assignment notification!');
|
||||||
|
}
|
||||||
|
console.log(' PASSED: Intern received project assignment notification over WebSocket.\n');
|
||||||
|
|
||||||
|
// 7. Mentor creates a task in the project
|
||||||
|
console.log(`7. Mentor creating a task in project ID ${createdProjectId}...`);
|
||||||
|
const createTaskRes = await axios.post(`${BASE_URL}/task-api/projects/${createdProjectId}/tasks`, {
|
||||||
|
title: 'Real-time Task',
|
||||||
|
description: 'Submit this task to test mentor notifications.',
|
||||||
|
deadline_date: '2026-08-01',
|
||||||
|
specific_time: '23:59',
|
||||||
|
submission_type: 'url_link'
|
||||||
|
}, getHeader(mentorToken));
|
||||||
|
taskId = createTaskRes.data.data.id;
|
||||||
|
console.log(` Task created successfully! ID: ${taskId}`);
|
||||||
|
|
||||||
|
// Wait for the WS notification to arrive
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Check if intern received "Tugas Baru Ditambahkan" notification
|
||||||
|
const taskNotif = internReceivedNotifs.find(n => n.title === 'Tugas Baru Ditambahkan');
|
||||||
|
if (!taskNotif) {
|
||||||
|
throw new Error('TEST FAILED: Intern did not receive real-time task creation notification!');
|
||||||
|
}
|
||||||
|
console.log(' PASSED: Intern received task creation notification over WebSocket.\n');
|
||||||
|
|
||||||
|
// 8. Intern submits the task
|
||||||
|
console.log(`8. Intern submitting task ID ${taskId}...`);
|
||||||
|
await axios.post(`${BASE_URL}/task-api/tasks/${taskId}/submissions`, {
|
||||||
|
url_link: 'https://github.com/rafiathallah/notification-system'
|
||||||
|
}, getHeader(internToken));
|
||||||
|
console.log(' Task submitted successfully!');
|
||||||
|
|
||||||
|
// Wait for the WS notification to arrive
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Check if mentor received "Pengiriman Tugas Baru" notification
|
||||||
|
const submissionNotif = mentorReceivedNotifs.find(n => n.title === 'Pengiriman Tugas Baru');
|
||||||
|
if (!submissionNotif) {
|
||||||
|
throw new Error('TEST FAILED: Mentor did not receive real-time task submission notification!');
|
||||||
|
}
|
||||||
|
console.log(' PASSED: Mentor received task submission notification over WebSocket.\n');
|
||||||
|
|
||||||
|
// 9. Intern retrieves their notifications via HTTP
|
||||||
|
console.log('9. Intern fetching notifications via GET /notification-api...');
|
||||||
|
const getNotifsRes = await axios.get(`${BASE_URL}/notification-api`, getHeader(internToken));
|
||||||
|
const dbNotifications = getNotifsRes.data.data;
|
||||||
|
console.log(` Notifications found: ${dbNotifications.length}`);
|
||||||
|
dbNotifications.forEach(n => console.log(` - [ID: ${n.id}] [Read: ${n.is_read}] ${n.title}: ${n.message}`));
|
||||||
|
|
||||||
|
if (dbNotifications.length < 2) {
|
||||||
|
throw new Error('TEST FAILED: Intern notifications list in database is incomplete!');
|
||||||
|
}
|
||||||
|
const hasUnread = dbNotifications.every(n => n.is_read === false);
|
||||||
|
if (!hasUnread) {
|
||||||
|
throw new Error('TEST FAILED: Notifications should be unread by default!');
|
||||||
|
}
|
||||||
|
console.log(' PASSED: HTTP notification retrieval correct.\n');
|
||||||
|
|
||||||
|
// 10. Intern marks a single notification as read
|
||||||
|
const targetNotifId = dbNotifications[0].id;
|
||||||
|
console.log(`10. Intern marking notification ID ${targetNotifId} as read...`);
|
||||||
|
const markReadRes = await axios.patch(`${BASE_URL}/notification-api/${targetNotifId}/read`, {}, getHeader(internToken));
|
||||||
|
|
||||||
|
if (!markReadRes.data.data.is_read) {
|
||||||
|
throw new Error('TEST FAILED: Notification status did not update to read!');
|
||||||
|
}
|
||||||
|
console.log(' PASSED: Single notification marked as read.\n');
|
||||||
|
|
||||||
|
// 11. Intern marks all notifications as read
|
||||||
|
console.log('11. Intern marking all remaining notifications as read...');
|
||||||
|
await axios.patch(`${BASE_URL}/notification-api/read-all`, {}, getHeader(internToken));
|
||||||
|
|
||||||
|
const recheckNotifsRes = await axios.get(`${BASE_URL}/notification-api`, getHeader(internToken));
|
||||||
|
const updatedNotifications = recheckNotifsRes.data.data;
|
||||||
|
const allRead = updatedNotifications.every(n => n.is_read === true);
|
||||||
|
if (!allRead) {
|
||||||
|
throw new Error('TEST FAILED: Not all notifications were marked as read!');
|
||||||
|
}
|
||||||
|
console.log(' PASSED: All notifications marked as read successfully.\n');
|
||||||
|
|
||||||
|
console.log('=== ALL INTEGRATION TESTS PASSED SUCCESSFULLY! ===');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('=== TEST EXECUTION ENCOUNTERED AN ERROR ===');
|
||||||
|
if (err.response) {
|
||||||
|
console.error(`Status: ${err.response.status}`);
|
||||||
|
console.error('Response Data:', JSON.stringify(err.response.data, null, 2));
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
// Cleanup WebSocket connections
|
||||||
|
if (internSocket) internSocket.close();
|
||||||
|
if (mentorSocket) mentorSocket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests();
|
||||||
Reference in New Issue
Block a user