feat: ngebuat API tambahan dan upload middleware
This commit is contained in:
Generated
+211
-2
@@ -16,7 +16,9 @@
|
|||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"morgan": "^1.10.0"
|
"morgan": "^1.10.0",
|
||||||
|
"multer": "^2.1.1",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
|
"@types/multer": "^2.1.0",
|
||||||
"@types/node": "^20.12.12",
|
"@types/node": "^20.12.12",
|
||||||
"prisma": "^6.19.3",
|
"prisma": "^6.19.3",
|
||||||
"rimraf": "^5.0.7",
|
"rimraf": "^5.0.7",
|
||||||
@@ -770,6 +773,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/multer": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.19.41",
|
"version": "20.19.41",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
||||||
@@ -881,6 +894,15 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
@@ -921,6 +943,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/append-field": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/arg": {
|
"node_modules/arg": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
@@ -1038,9 +1066,19 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/busboy": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||||
|
"dependencies": {
|
||||||
|
"streamsearch": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -1108,6 +1146,19 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
@@ -1134,6 +1185,15 @@
|
|||||||
"consola": "^3.2.3"
|
"consola": "^3.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -1161,6 +1221,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/concat-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
|
"engines": [
|
||||||
|
"node >= 6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.0.2",
|
||||||
|
"typedarray": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/confbox": {
|
"node_modules/confbox": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||||
@@ -1231,6 +1306,18 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/create-require": {
|
"node_modules/create-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
@@ -1630,6 +1717,15 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
@@ -2226,6 +2322,25 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/multer": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"append-field": "^1.0.0",
|
||||||
|
"busboy": "^1.6.0",
|
||||||
|
"concat-stream": "^2.0.0",
|
||||||
|
"type-is": "^1.6.18"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
@@ -2548,6 +2663,20 @@
|
|||||||
"destr": "^2.0.3"
|
"destr": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -2818,6 +2947,18 @@
|
|||||||
"source-map": "^0.6.0"
|
"source-map": "^0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
@@ -2827,6 +2968,23 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/streamsearch": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
@@ -3228,6 +3386,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedarray": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -3259,6 +3423,12 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
@@ -3300,6 +3470,24 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||||
@@ -3405,6 +3593,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
|
|||||||
+4
-1
@@ -24,7 +24,9 @@
|
|||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"morgan": "^1.10.0"
|
"morgan": "^1.10.0",
|
||||||
|
"multer": "^2.1.1",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
|
"@types/multer": "^2.1.0",
|
||||||
"@types/node": "^20.12.12",
|
"@types/node": "^20.12.12",
|
||||||
"prisma": "^6.19.3",
|
"prisma": "^6.19.3",
|
||||||
"rimraf": "^5.0.7",
|
"rimraf": "^5.0.7",
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { PrismaClient } from '../src/generated/prisma/client';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('=== STARTING DEVELOPER DUMMY SEEDING ===');
|
||||||
|
|
||||||
|
const adminEmail = 'admin@internify.com';
|
||||||
|
const adminPassword = 'AdminInternify123!';
|
||||||
|
const passwordHash = await bcrypt.hash(adminPassword, 10);
|
||||||
|
|
||||||
|
let adminUser = await prisma.user.findUnique({ where: { email: adminEmail } });
|
||||||
|
if (!adminUser) {
|
||||||
|
adminUser = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: adminEmail,
|
||||||
|
passwordHash,
|
||||||
|
role: 'ADMIN',
|
||||||
|
admin: {
|
||||||
|
create: {
|
||||||
|
namaDepan: 'Super',
|
||||||
|
namaBelakang: 'Admin'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(' Admin created:', adminEmail);
|
||||||
|
} else {
|
||||||
|
console.log(' Admin already exists:', adminEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(' Seeding Batches...');
|
||||||
|
const batchesData = [
|
||||||
|
{ batchNumber: 1, isActive: false },
|
||||||
|
{ batchNumber: 2, isActive: false },
|
||||||
|
{ batchNumber: 3, isActive: true } // Active Batch
|
||||||
|
];
|
||||||
|
|
||||||
|
const batches: any[] = [];
|
||||||
|
for (const b of batchesData) {
|
||||||
|
let dbBatch = await prisma.batch.findUnique({ where: { batchNumber: b.batchNumber } });
|
||||||
|
if (!dbBatch) {
|
||||||
|
dbBatch = await prisma.batch.create({ data: b });
|
||||||
|
}
|
||||||
|
batches.push(dbBatch);
|
||||||
|
}
|
||||||
|
console.log(` Seeding Batches complete! Created/Found ${batches.length} batches.`);
|
||||||
|
|
||||||
|
const activeBatch = batches.find(b => b.isActive) || batches[2];
|
||||||
|
|
||||||
|
console.log(' Seeding Student Users...');
|
||||||
|
const studentsData = [
|
||||||
|
{
|
||||||
|
email: 'alice@example.com',
|
||||||
|
namaDepan: 'Alice',
|
||||||
|
namaBelakang: 'Smith',
|
||||||
|
kontak: '08111222333',
|
||||||
|
jurusan: 'Informatika',
|
||||||
|
universitas: 'Institut Teknologi Bandung',
|
||||||
|
negara: 'Indonesia',
|
||||||
|
cvPath: '/uploads/docs/alice-cv.pdf',
|
||||||
|
portofolioPath: '/uploads/docs/alice-portfolio.pdf',
|
||||||
|
motivasi: 'Saya sangat tertarik dengan UI/UX design dan ingin mempraktekkannya.',
|
||||||
|
relevantSkills: 'Figma, Adobe XD, User Research'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'bob@example.com',
|
||||||
|
namaDepan: 'Bob',
|
||||||
|
namaBelakang: 'Johnson',
|
||||||
|
kontak: '08222333444',
|
||||||
|
jurusan: 'Sistem Informasi',
|
||||||
|
universitas: 'Universitas Indonesia',
|
||||||
|
negara: 'Indonesia',
|
||||||
|
cvPath: '/uploads/docs/bob-cv.pdf',
|
||||||
|
portofolioPath: '/uploads/docs/bob-portfolio.pdf',
|
||||||
|
motivasi: 'Ingin mengembangkan keahlian React dan Tailwind CSS.',
|
||||||
|
relevantSkills: 'React, HTML, CSS, JavaScript'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'charlie@example.com',
|
||||||
|
namaDepan: 'Charlie',
|
||||||
|
namaBelakang: 'Brown',
|
||||||
|
kontak: '08333444555',
|
||||||
|
jurusan: 'Teknik Komputer',
|
||||||
|
universitas: 'Telkom University',
|
||||||
|
negara: 'Indonesia',
|
||||||
|
cvPath: '/uploads/docs/charlie-cv.pdf',
|
||||||
|
portofolioPath: '/uploads/docs/charlie-portfolio.pdf',
|
||||||
|
motivasi: 'Suka membangun server scalable dan merancang arsitektur API.',
|
||||||
|
relevantSkills: 'Node.js, Express, MySQL, TypeScript'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const mahasiswaList: any[] = [];
|
||||||
|
for (const s of studentsData) {
|
||||||
|
let user = await prisma.user.findUnique({
|
||||||
|
where: { email: s.email },
|
||||||
|
include: { mahasiswa: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
const hash = await bcrypt.hash('Student123!', 10);
|
||||||
|
user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: s.email,
|
||||||
|
passwordHash: hash,
|
||||||
|
role: 'STUDENT',
|
||||||
|
mahasiswa: {
|
||||||
|
create: {
|
||||||
|
namaDepan: s.namaDepan,
|
||||||
|
namaBelakang: s.namaBelakang,
|
||||||
|
kontak: s.kontak,
|
||||||
|
jurusan: s.jurusan,
|
||||||
|
universitas: s.universitas,
|
||||||
|
negara: s.negara,
|
||||||
|
cvPath: s.cvPath,
|
||||||
|
portofolioPath: s.portofolioPath,
|
||||||
|
motivasi: s.motivasi,
|
||||||
|
relevantSkills: s.relevantSkills
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: { mahasiswa: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mahasiswaList.push(user.mahasiswa);
|
||||||
|
}
|
||||||
|
console.log(` Student seeding complete! Created/Found ${mahasiswaList.length} students.`);
|
||||||
|
|
||||||
|
console.log(' Seeding Lowongan Listings...');
|
||||||
|
const lowongansData = [
|
||||||
|
{
|
||||||
|
id: 'lowongan-ui-ux',
|
||||||
|
posisi: 'UI/UX Designer Intern',
|
||||||
|
kelompokPeminatan: 'Design & Creative',
|
||||||
|
imagePath: '/uploads/images/ui-ux-design.png',
|
||||||
|
jobdesk: '<p>Mendesain antarmuka aplikasi mobile dan web, melakukan user testing, dan wireframing.</p>',
|
||||||
|
lokasi: 'Onsite - Bandung',
|
||||||
|
kualifikasi: 'Menguasai Figma, memiliki portofolio desain UI/UX, kreatif.',
|
||||||
|
benefit: 'Sertifikat magang resmi, uang transport harian, mentoring 1-on-1.',
|
||||||
|
paid: 'PAID' as any,
|
||||||
|
statusLowongan: 'DIBUKA' as any
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lowongan-frontend',
|
||||||
|
posisi: 'Frontend Developer Intern',
|
||||||
|
kelompokPeminatan: 'Web Engineering',
|
||||||
|
imagePath: '/uploads/images/frontend-dev.png',
|
||||||
|
jobdesk: '<p>Mengembangkan antarmuka responsif menggunakan React dan Tailwind CSS.</p>',
|
||||||
|
lokasi: 'Remote',
|
||||||
|
kualifikasi: 'Memahami React JS, ES6 JavaScript, HTML, CSS.',
|
||||||
|
benefit: 'Sertifikat magang resmi, jam kerja fleksibel, free access learning course.',
|
||||||
|
paid: 'UNPAID' as any,
|
||||||
|
statusLowongan: 'DIBUKA' as any
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lowongan-backend',
|
||||||
|
posisi: 'Backend Developer Intern',
|
||||||
|
kelompokPeminatan: 'Web Engineering',
|
||||||
|
imagePath: '/uploads/images/backend-dev.png',
|
||||||
|
jobdesk: '<p>Membangun REST API, mengoptimalkan query database, dan integrasi backend services.</p>',
|
||||||
|
lokasi: 'Hybrid - Jakarta',
|
||||||
|
kualifikasi: 'Memahami Node.js, Express, dan Relational Database seperti MySQL.',
|
||||||
|
benefit: 'Sertifikat magang resmi, konversi SKS, coworking space access.',
|
||||||
|
paid: 'PAID' as any,
|
||||||
|
statusLowongan: 'DIBUKA' as any
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const lowonganList: any[] = [];
|
||||||
|
for (const l of lowongansData) {
|
||||||
|
let dbLowongan = await prisma.lowonganMagang.findUnique({ where: { id: l.id } });
|
||||||
|
if (!dbLowongan) {
|
||||||
|
dbLowongan = await prisma.lowonganMagang.create({
|
||||||
|
data: {
|
||||||
|
id: l.id,
|
||||||
|
idBatch: activeBatch.id,
|
||||||
|
posisi: l.posisi,
|
||||||
|
kelompokPeminatan: l.kelompokPeminatan,
|
||||||
|
imagePath: l.imagePath,
|
||||||
|
jobdesk: l.jobdesk,
|
||||||
|
lokasi: l.lokasi,
|
||||||
|
kualifikasi: l.kualifikasi,
|
||||||
|
benefit: l.benefit,
|
||||||
|
durasiAwal: new Date(),
|
||||||
|
durasiAkhir: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now
|
||||||
|
statusLowongan: l.statusLowongan,
|
||||||
|
paid: l.paid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
lowonganList.push(dbLowongan);
|
||||||
|
}
|
||||||
|
console.log(` Lowongan seeding complete! Created/Found ${lowonganList.length} listings.`);
|
||||||
|
|
||||||
|
console.log(' Seeding Lamaran Records...');
|
||||||
|
const lamaranData = [
|
||||||
|
{
|
||||||
|
idMahasiswa: mahasiswaList[0].id,
|
||||||
|
idLowonganMagang: lowonganList[0].id,
|
||||||
|
status: 'DIPROSES' as any
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idMahasiswa: mahasiswaList[1].id,
|
||||||
|
idLowonganMagang: lowonganList[1].id,
|
||||||
|
status: 'DIPROSES' as any
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idMahasiswa: mahasiswaList[2].id,
|
||||||
|
idLowonganMagang: lowonganList[2].id,
|
||||||
|
status: 'DIPROSES' as any
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let lamaranCount = 0;
|
||||||
|
for (const lam of lamaranData) {
|
||||||
|
const existing = await prisma.lamaranMagang.findFirst({
|
||||||
|
where: {
|
||||||
|
idMahasiswa: lam.idMahasiswa,
|
||||||
|
idLowonganMagang: lam.idLowonganMagang
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
await prisma.lamaranMagang.create({
|
||||||
|
data: {
|
||||||
|
idMahasiswa: lam.idMahasiswa,
|
||||||
|
idLowonganMagang: lam.idLowonganMagang,
|
||||||
|
status: lam.status,
|
||||||
|
batch: activeBatch.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lamaranCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(` Lamaran seeding complete! Created ${lamaranCount} new applications.`);
|
||||||
|
|
||||||
|
console.log('\n=== DEVELOPER SEEDING COMPLETED SUCCESSFULLY ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch(e => {
|
||||||
|
console.error('Seeding failed:', e);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
+9
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import path from 'path';
|
||||||
import express, { Request, Response, NextFunction } from 'express';
|
import express, { Request, Response, NextFunction } from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
@@ -8,10 +9,17 @@ import { errorHandler, CustomError } from './middleware/errorHandler';
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
app.use(cors());
|
app.use(cors({
|
||||||
|
origin: true,
|
||||||
|
credentials: true,
|
||||||
|
}));
|
||||||
|
|
||||||
app.use(morgan('dev'));
|
app.use(morgan('dev'));
|
||||||
|
|
||||||
|
// File upload statis biar bisa diakses lewat /uploads atau /api/uploads
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
|
||||||
|
app.use('/api/uploads', express.static(path.join(__dirname, '../uploads')));
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import multer from 'multer';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (_req, file, cb) => {
|
||||||
|
let folder = 'uploads/';
|
||||||
|
if (file.fieldname === 'cv' || file.fieldname === 'portofolio') {
|
||||||
|
folder += 'docs/';
|
||||||
|
} else {
|
||||||
|
folder += 'images/';
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(folder, { recursive: true });
|
||||||
|
cb(null, folder);
|
||||||
|
},
|
||||||
|
filename: (_req, file, cb) => {
|
||||||
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||||
|
const ext = path.extname(file.originalname);
|
||||||
|
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileFilter = (_req: any, file: any, cb: any) => {
|
||||||
|
if (file.fieldname === 'cv' || file.fieldname === 'portofolio') {
|
||||||
|
if (file.mimetype === 'application/pdf') {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('CV dan Portofolio harus dalam format PDF!'), false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (file.mimetype.startsWith('image/')) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('File upload harus berupa gambar!'), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
fileFilter: fileFilter,
|
||||||
|
limits: {
|
||||||
|
fileSize: 10 * 1024 * 1024, // Maksimal 10MB yaa, nanti paling bisa diganti oleh admin melalui website
|
||||||
|
}
|
||||||
|
});
|
||||||
+5
-3
@@ -17,7 +17,7 @@ const generateToken = (payload: { id: string; email: string; role: string }) =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @route POST /api/auth/register
|
* @route POST /api/auth/register
|
||||||
* @desc Mendaftarkan user baru dengan role STUDENT (Mahasiswa)
|
* @desc Daftarin user baru dgn role STUDENT (Mahasiswa)
|
||||||
* @access Public
|
* @access Public
|
||||||
*/
|
*/
|
||||||
router.post('/register', async (req: Request, res: Response, next: NextFunction) => {
|
router.post('/register', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -101,7 +101,7 @@ router.post('/register', async (req: Request, res: Response, next: NextFunction)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @route POST /api/auth/login
|
* @route POST /api/auth/login
|
||||||
* @desc Autentikasi user dan dapatin token
|
* @desc Autentikasi user biar dapet token
|
||||||
* @access Public
|
* @access Public
|
||||||
*/
|
*/
|
||||||
router.post('/login', async (req: Request, res: Response, next: NextFunction) => {
|
router.post('/login', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -145,6 +145,7 @@ router.post('/login', async (req: Request, res: Response, next: NextFunction) =>
|
|||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
data: {
|
||||||
token,
|
token,
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -152,6 +153,7 @@ router.post('/login', async (req: Request, res: Response, next: NextFunction) =>
|
|||||||
role: user.role,
|
role: user.role,
|
||||||
profile,
|
profile,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -160,7 +162,7 @@ router.post('/login', async (req: Request, res: Response, next: NextFunction) =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @route GET /api/auth/profile
|
* @route GET /api/auth/profile
|
||||||
* @desc Dapatin profile user yang sedang login
|
* @desc Dapetin profil user yg lagi login
|
||||||
* @access Private
|
* @access Private
|
||||||
*/
|
*/
|
||||||
router.get('/profile', protect, async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
router.get('/profile', protect, async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||||
|
import prisma from '../lib/prisma';
|
||||||
|
import { CustomError } from '../middleware/errorHandler';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Map data faq dari DB ke format frontend
|
||||||
|
function mapToFrontend(faq: any) {
|
||||||
|
return {
|
||||||
|
id: faq.id,
|
||||||
|
pertanyaan: faq.pertanyaan,
|
||||||
|
jawaban: faq.jawaban,
|
||||||
|
created_at: faq.createdAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/faq-api/get
|
||||||
|
* @desc Dapetin semua list FAQ
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const faqs = await prisma.faq.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: faqs.map(mapToFrontend)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/faq-api/get/:id
|
||||||
|
* @desc Dapetin detail satu FAQ
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const faq = await prisma.faq.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!faq) {
|
||||||
|
const error: CustomError = new Error('FAQ tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: mapToFrontend(faq)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/faq-api/add
|
||||||
|
* @desc Tambah FAQ baru (khusus admin ya)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.post('/add', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { pertanyaan, jawaban } = req.body;
|
||||||
|
|
||||||
|
if (!pertanyaan || !jawaban) {
|
||||||
|
const error: CustomError = new Error('Pertanyaan dan jawaban harus diisi');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const faq = await prisma.faq.create({
|
||||||
|
data: {
|
||||||
|
pertanyaan,
|
||||||
|
jawaban
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
status: true,
|
||||||
|
message: 'FAQ berhasil ditambahkan',
|
||||||
|
data: mapToFrontend(faq)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route PATCH /api/faq-api/update/:id
|
||||||
|
* @desc Update data FAQ (khusus admin)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { pertanyaan, jawaban } = req.body;
|
||||||
|
|
||||||
|
const existing = await prisma.faq.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const error: CustomError = new Error('FAQ tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.faq.update({
|
||||||
|
where: { id: parseInt(id, 10) },
|
||||||
|
data: {
|
||||||
|
pertanyaan: pertanyaan ?? existing.pertanyaan,
|
||||||
|
jawaban: jawaban ?? existing.jawaban
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'FAQ berhasil diupdate',
|
||||||
|
data: mapToFrontend(updated)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route DELETE /api/faq-api/delete/:id
|
||||||
|
* @desc Hapus data FAQ (khusus admin)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const existing = await prisma.faq.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const error: CustomError = new Error('FAQ tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.faq.delete({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'FAQ berhasil dihapus'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||||
|
import prisma from '../lib/prisma';
|
||||||
|
import { upload } from '../middleware/upload';
|
||||||
|
import { CustomError } from '../middleware/errorHandler';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Map data feedback dari DB ke format frontend
|
||||||
|
function mapToFrontend(feedback: any) {
|
||||||
|
return {
|
||||||
|
id: feedback.id,
|
||||||
|
nama: feedback.nama,
|
||||||
|
universitas: feedback.universitas,
|
||||||
|
posisi: feedback.posisi,
|
||||||
|
batch: feedback.batch,
|
||||||
|
tahun: feedback.tahun,
|
||||||
|
pesan: feedback.pesan,
|
||||||
|
image_path: feedback.imagePath,
|
||||||
|
createdAt: feedback.createdAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/feedback-api/get
|
||||||
|
* @desc Dapetin semua feedback dari mahasiswa
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const feedbacks = await prisma.feedback.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: feedbacks.map(mapToFrontend)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/feedback-api/get/:id
|
||||||
|
* @desc Dapetin detail satu feedback
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const feedback = await prisma.feedback.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!feedback) {
|
||||||
|
const error: CustomError = new Error('Feedback tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: mapToFrontend(feedback)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/feedback-api/add
|
||||||
|
* @desc Tambah feedback baru (bisa admin / student)
|
||||||
|
* @access Private (Admin/Student)
|
||||||
|
*/
|
||||||
|
router.post('/add', protect, upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { nama, universitas, posisi, batch, tahun, pesan } = req.body;
|
||||||
|
|
||||||
|
if (!nama || !universitas || !posisi || !batch || !tahun || !pesan) {
|
||||||
|
const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBatch = parseInt(batch, 10);
|
||||||
|
const parsedTahun = parseInt(tahun, 10);
|
||||||
|
|
||||||
|
if (isNaN(parsedBatch) || isNaN(parsedTahun)) {
|
||||||
|
const error: CustomError = new Error('Batch dan tahun harus berupa angka');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imagePath = req.file ? `/uploads/images/${req.file.filename}` : '';
|
||||||
|
|
||||||
|
const feedback = await prisma.feedback.create({
|
||||||
|
data: {
|
||||||
|
nama,
|
||||||
|
universitas,
|
||||||
|
posisi,
|
||||||
|
batch: parsedBatch,
|
||||||
|
tahun: parsedTahun,
|
||||||
|
pesan,
|
||||||
|
imagePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Feedback berhasil ditambahkan',
|
||||||
|
data: mapToFrontend(feedback)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route PATCH /api/feedback-api/update/:id
|
||||||
|
* @desc Update data feedback (bisa admin / student)
|
||||||
|
* @access Private (Admin/Student)
|
||||||
|
*/
|
||||||
|
router.patch('/update/:id', protect, upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { nama, universitas, posisi, batch, tahun, pesan } = req.body;
|
||||||
|
|
||||||
|
const existing = await prisma.feedback.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const error: CustomError = new Error('Feedback tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let imagePath = existing.imagePath;
|
||||||
|
if (req.file) {
|
||||||
|
imagePath = `/uploads/images/${req.file.filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBatch = batch ? parseInt(batch, 10) : undefined;
|
||||||
|
const parsedTahun = tahun ? parseInt(tahun, 10) : undefined;
|
||||||
|
|
||||||
|
if ((batch && isNaN(parsedBatch as number)) || (tahun && isNaN(parsedTahun as number))) {
|
||||||
|
const error: CustomError = new Error('Batch dan tahun harus berupa angka');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.feedback.update({
|
||||||
|
where: { id: parseInt(id, 10) },
|
||||||
|
data: {
|
||||||
|
nama: nama ?? existing.nama,
|
||||||
|
universitas: universitas ?? existing.universitas,
|
||||||
|
posisi: posisi ?? existing.posisi,
|
||||||
|
batch: parsedBatch ?? existing.batch,
|
||||||
|
tahun: parsedTahun ?? existing.tahun,
|
||||||
|
pesan: pesan ?? existing.pesan,
|
||||||
|
imagePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Feedback berhasil diupdate',
|
||||||
|
data: mapToFrontend(updated)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route DELETE /api/feedback-api/delete/:id
|
||||||
|
* @desc Hapus feedback (khusus admin)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const existing = await prisma.feedback.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const error: CustomError = new Error('Feedback tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.feedback.delete({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Feedback berhasil dihapus'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,10 +1,23 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import prisma from '../lib/prisma';
|
import prisma from '../lib/prisma';
|
||||||
import authRouter from './auth';
|
import authRouter from './auth';
|
||||||
|
import lowonganRouter from './lowongan';
|
||||||
|
import lamaranRouter from './lamaran';
|
||||||
|
import partnershipRouter from './partnership';
|
||||||
|
import researchRouter from './research';
|
||||||
|
import feedbackRouter from './feedback';
|
||||||
|
import faqRouter from './faq';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use('/auth', authRouter);
|
router.use('/auth', authRouter);
|
||||||
|
router.use('/auth-api', authRouter);
|
||||||
|
router.use('/lowongan-magang-api', lowonganRouter);
|
||||||
|
router.use('/lamaran-magang-api', lamaranRouter);
|
||||||
|
router.use('/partnership-api', partnershipRouter);
|
||||||
|
router.use('/hasil-research-api', researchRouter);
|
||||||
|
router.use('/feedback-api', feedbackRouter);
|
||||||
|
router.use('/faq-api', faqRouter);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @route GET /api/health
|
* @route GET /api/health
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||||
|
import prisma from '../lib/prisma';
|
||||||
|
import { upload } from '../middleware/upload';
|
||||||
|
import { CustomError } from '../middleware/errorHandler';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Map data lamaran dari DB ke format frontend
|
||||||
|
function mapToFrontend(lamaran: any) {
|
||||||
|
return {
|
||||||
|
id: lamaran.id,
|
||||||
|
status: (lamaran.status || 'diproses').toLowerCase(),
|
||||||
|
batch: lamaran.batch,
|
||||||
|
createdAt: lamaran.createdAt,
|
||||||
|
updatedAt: lamaran.updatedAt,
|
||||||
|
mahasiswa: lamaran.mahasiswa ? {
|
||||||
|
id: lamaran.mahasiswa.id,
|
||||||
|
nama_depan: lamaran.mahasiswa.namaDepan,
|
||||||
|
nama_belakang: lamaran.mahasiswa.namaBelakang || '',
|
||||||
|
email: lamaran.mahasiswa.user?.email || '',
|
||||||
|
kontak: lamaran.mahasiswa.kontak,
|
||||||
|
jurusan: lamaran.mahasiswa.jurusan,
|
||||||
|
universitas: lamaran.mahasiswa.universitas,
|
||||||
|
negara: lamaran.mahasiswa.negara,
|
||||||
|
cv_path: lamaran.mahasiswa.cvPath,
|
||||||
|
portofolio_path: lamaran.mahasiswa.portofolioPath,
|
||||||
|
motivasi: lamaran.mahasiswa.motivasi,
|
||||||
|
relevant_skills: lamaran.mahasiswa.relevantSkills
|
||||||
|
} : null,
|
||||||
|
lowongan_magang: lamaran.lowonganMagang ? {
|
||||||
|
id: lamaran.lowonganMagang.id,
|
||||||
|
posisi: lamaran.lowonganMagang.posisi,
|
||||||
|
kelompok_peminatan: lamaran.lowonganMagang.kelompokPeminatan
|
||||||
|
} : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/lamaran-magang-api/add/:lowonganId
|
||||||
|
* @desc Kirim pendaftaran magang baru
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
'/add/:lowonganId',
|
||||||
|
upload.fields([
|
||||||
|
{ name: 'cv', maxCount: 1 },
|
||||||
|
{ name: 'portofolio', maxCount: 1 }
|
||||||
|
]),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { lowonganId } = req.params;
|
||||||
|
const {
|
||||||
|
nama_depan,
|
||||||
|
nama_belakang,
|
||||||
|
email,
|
||||||
|
kontak,
|
||||||
|
universitas,
|
||||||
|
negara,
|
||||||
|
jurusan,
|
||||||
|
batch,
|
||||||
|
motivasi,
|
||||||
|
relevant_skills
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!nama_depan || !email || !kontak || !universitas || !negara || !jurusan || !motivasi) {
|
||||||
|
const error: CustomError = new Error('Mohon lengkapi semua field wajib yang diperlukan');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = req.files as { [fieldname: string]: Express.Multer.File[] };
|
||||||
|
const cvFile = files?.['cv']?.[0];
|
||||||
|
const portofolioFile = files?.['portofolio']?.[0];
|
||||||
|
|
||||||
|
if (!cvFile || !portofolioFile) {
|
||||||
|
const error: CustomError = new Error('CV dan Portofolio wajib diunggah!');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowongan = await prisma.lowonganMagang.findUnique({
|
||||||
|
where: { id: lowonganId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!lowongan) {
|
||||||
|
const error: CustomError = new Error('Lowongan magang tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await prisma.user.findUnique({
|
||||||
|
where: { email },
|
||||||
|
include: { mahasiswa: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const cvPath = `/uploads/docs/${cvFile.filename}`;
|
||||||
|
const portofolioPath = `/uploads/docs/${portofolioFile.filename}`;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// Kalo user belom terdaftar, kita buatin akun STUDENT baru
|
||||||
|
const randomPassword = Math.random().toString(36).substring(2, 15);
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
const passwordHash = await bcrypt.hash(randomPassword, salt);
|
||||||
|
|
||||||
|
user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
passwordHash,
|
||||||
|
role: 'STUDENT',
|
||||||
|
mahasiswa: {
|
||||||
|
create: {
|
||||||
|
namaDepan: nama_depan,
|
||||||
|
namaBelakang: nama_belakang || null,
|
||||||
|
kontak,
|
||||||
|
jurusan,
|
||||||
|
universitas,
|
||||||
|
negara,
|
||||||
|
cvPath,
|
||||||
|
portofolioPath,
|
||||||
|
motivasi,
|
||||||
|
relevantSkills: relevant_skills || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: { mahasiswa: true }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Kalo user udah ada, kita update data profil mahasiswanya aja
|
||||||
|
if (user.mahasiswa) {
|
||||||
|
await prisma.mahasiswa.update({
|
||||||
|
where: { id: user.mahasiswa.id },
|
||||||
|
data: {
|
||||||
|
namaDepan: nama_depan,
|
||||||
|
namaBelakang: nama_belakang || user.mahasiswa.namaBelakang,
|
||||||
|
kontak: kontak || user.mahasiswa.kontak,
|
||||||
|
jurusan: jurusan || user.mahasiswa.jurusan,
|
||||||
|
universitas: universitas || user.mahasiswa.universitas,
|
||||||
|
negara: negara || user.mahasiswa.negara,
|
||||||
|
cvPath,
|
||||||
|
portofolioPath,
|
||||||
|
motivasi,
|
||||||
|
relevantSkills: relevant_skills || user.mahasiswa.relevantSkills
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBatch = batch ? parseInt(batch, 10) : lowongan.idBatch;
|
||||||
|
|
||||||
|
const lamaran = await prisma.lamaranMagang.create({
|
||||||
|
data: {
|
||||||
|
idMahasiswa: user.mahasiswa!.id,
|
||||||
|
idLowonganMagang: lowongan.id,
|
||||||
|
status: 'DIPROSES',
|
||||||
|
batch: parsedBatch
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
mahasiswa: {
|
||||||
|
include: { user: true }
|
||||||
|
},
|
||||||
|
lowonganMagang: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Pendaftaran magang berhasil dikirim',
|
||||||
|
data: mapToFrontend(lamaran)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/lamaran-magang-api/get
|
||||||
|
* @desc Dapetin semua data pelamar (admin aja ya)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.get('/get', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const applications = await prisma.lamaranMagang.findMany({
|
||||||
|
include: {
|
||||||
|
mahasiswa: {
|
||||||
|
include: { user: true }
|
||||||
|
},
|
||||||
|
lowonganMagang: true
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: applications.map(mapToFrontend)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/lamaran-magang-api/get/:id
|
||||||
|
* @desc Dapetin detail satu lamaran berdasarkan id
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.get('/get/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const application = await prisma.lamaranMagang.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) },
|
||||||
|
include: {
|
||||||
|
mahasiswa: {
|
||||||
|
include: { user: true }
|
||||||
|
},
|
||||||
|
lowonganMagang: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!application) {
|
||||||
|
const error: CustomError = new Error('Lamaran tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: mapToFrontend(application)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route PATCH /api/lamaran-magang-api/update/:id
|
||||||
|
* @desc Ganti status lamaran (diterima, ditolak, dll)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { status } = req.body;
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
const error: CustomError = new Error('Mohon masukkan status baru');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedStatus = status.toUpperCase();
|
||||||
|
if (formattedStatus !== 'DITERIMA' && formattedStatus !== 'DITOLAK' && formattedStatus !== 'DIPROSES') {
|
||||||
|
const error: CustomError = new Error('Status tidak valid');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.lamaranMagang.update({
|
||||||
|
where: { id: parseInt(id, 10) },
|
||||||
|
data: {
|
||||||
|
status: formattedStatus as any
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
mahasiswa: {
|
||||||
|
include: { user: true }
|
||||||
|
},
|
||||||
|
lowonganMagang: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Status lamaran berhasil diupdate',
|
||||||
|
data: mapToFrontend(updated)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route DELETE /api/lamaran-magang-api/delete
|
||||||
|
* @desc Hapus semua data pelamar
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.delete('/delete', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
await prisma.lamaranMagang.deleteMany({});
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Seluruh data pelamar berhasil dihapus'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/lamaran-magang-api/export
|
||||||
|
* @desc Ekspor data pelamar ke file Excel (.xlsx)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.get('/export', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const applications = await prisma.lamaranMagang.findMany({
|
||||||
|
include: {
|
||||||
|
mahasiswa: {
|
||||||
|
include: { user: true }
|
||||||
|
},
|
||||||
|
lowonganMagang: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = applications.map((app) => ({
|
||||||
|
ID: app.id,
|
||||||
|
Nama: `${app.mahasiswa?.namaDepan} ${app.mahasiswa?.namaBelakang || ''}`.trim(),
|
||||||
|
Email: app.mahasiswa?.user?.email || '',
|
||||||
|
Kontak: app.mahasiswa?.kontak || '',
|
||||||
|
Universitas: app.mahasiswa?.universitas || '',
|
||||||
|
Jurusan: app.mahasiswa?.jurusan || '',
|
||||||
|
Negara: app.mahasiswa?.negara || '',
|
||||||
|
Posisi: app.lowonganMagang?.posisi || '',
|
||||||
|
Status: app.status,
|
||||||
|
Motivasi: app.mahasiswa?.motivasi || '',
|
||||||
|
Skills: app.mahasiswa?.relevantSkills || ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(rows);
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Applications');
|
||||||
|
|
||||||
|
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="data.xlsx"');
|
||||||
|
res.send(buffer);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||||
|
import prisma from '../lib/prisma';
|
||||||
|
import { upload } from '../middleware/upload';
|
||||||
|
import { CustomError } from '../middleware/errorHandler';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Nyari batch yg aktif, kalo ga ada ya kita bikinin yg baru
|
||||||
|
async function getOrCreateActiveBatch() {
|
||||||
|
let batch = await prisma.batch.findFirst({
|
||||||
|
where: { isActive: true }
|
||||||
|
});
|
||||||
|
if (!batch) {
|
||||||
|
batch = await prisma.batch.create({
|
||||||
|
data: {
|
||||||
|
batchNumber: 1,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return batch;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapToFrontend(lowongan: any) {
|
||||||
|
return {
|
||||||
|
id: lowongan.id,
|
||||||
|
id_batch: lowongan.idBatch,
|
||||||
|
posisi: lowongan.posisi,
|
||||||
|
kelompok_peminatan: lowongan.kelompokPeminatan,
|
||||||
|
image_path: lowongan.imagePath,
|
||||||
|
jobdesk: lowongan.jobdesk,
|
||||||
|
lokasi: lowongan.lokasi,
|
||||||
|
kualifikasi: lowongan.kualifikasi,
|
||||||
|
benefit: lowongan.benefit,
|
||||||
|
durasi_awal: lowongan.durasiAwal,
|
||||||
|
durasi_akhir: lowongan.durasiAkhir,
|
||||||
|
status_lowongan: lowongan.statusLowongan,
|
||||||
|
paid: lowongan.paid,
|
||||||
|
createdAt: lowongan.createdAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/lowongan-magang-api/get
|
||||||
|
* @desc Ambil semua daftar lowongan magang nih
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const lowongans = await prisma.lowonganMagang.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: lowongans.map(mapToFrontend)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/lowongan-magang-api/get/kelompok-all
|
||||||
|
* @desc Ambil semua kelompok peminatan yg unik
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/get/kelompok-all', async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const result = await prisma.lowonganMagang.findMany({
|
||||||
|
select: { kelompokPeminatan: true },
|
||||||
|
distinct: ['kelompokPeminatan']
|
||||||
|
});
|
||||||
|
const categories = result.map((r) => r.kelompokPeminatan);
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: categories
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/lowongan-magang-api/get/id/:id
|
||||||
|
* @desc Ambil detail lowongan magang berdasarkan id
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/get/id/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const lowongan = await prisma.lowonganMagang.findUnique({
|
||||||
|
where: { id }
|
||||||
|
});
|
||||||
|
if (!lowongan) {
|
||||||
|
const error: CustomError = new Error('Lowongan tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: mapToFrontend(lowongan)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/lowongan-magang-api/add
|
||||||
|
* @desc Tambah lowongan magang baru (khusus admin ya)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
posisi,
|
||||||
|
kelompok_peminatan,
|
||||||
|
lokasi,
|
||||||
|
jobdesk,
|
||||||
|
kualifikasi,
|
||||||
|
benefit,
|
||||||
|
durasi_awal,
|
||||||
|
durasi_akhir,
|
||||||
|
paid
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!posisi || !kelompok_peminatan || !lokasi || !jobdesk || !durasi_awal || !durasi_akhir || !paid) {
|
||||||
|
const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const batch = await getOrCreateActiveBatch();
|
||||||
|
|
||||||
|
const imagePath = req.file ? `/uploads/images/${req.file.filename}` : '';
|
||||||
|
|
||||||
|
const id = `lowongan-${Date.now()}`;
|
||||||
|
|
||||||
|
const lowongan = await prisma.lowonganMagang.create({
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
idBatch: batch.id,
|
||||||
|
posisi,
|
||||||
|
kelompokPeminatan: kelompok_peminatan,
|
||||||
|
imagePath,
|
||||||
|
jobdesk,
|
||||||
|
lokasi,
|
||||||
|
kualifikasi: kualifikasi || '',
|
||||||
|
benefit: benefit || '',
|
||||||
|
durasiAwal: new Date(durasi_awal),
|
||||||
|
durasiAkhir: new Date(durasi_akhir),
|
||||||
|
statusLowongan: 'DIBUKA',
|
||||||
|
paid: paid.toUpperCase() === 'PAID' ? 'PAID' : 'UNPAID'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Internship lowongan berhasil ditambahkan',
|
||||||
|
data: mapToFrontend(lowongan)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route PATCH /api/lowongan-magang-api/update/:id
|
||||||
|
* @desc Update data lowongan magang (khusus admin)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const {
|
||||||
|
posisi,
|
||||||
|
kelompok_peminatan,
|
||||||
|
lokasi,
|
||||||
|
jobdesk,
|
||||||
|
kualifikasi,
|
||||||
|
benefit,
|
||||||
|
durasi_awal,
|
||||||
|
durasi_akhir,
|
||||||
|
paid,
|
||||||
|
status_lowongan
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const existing = await prisma.lowonganMagang.findUnique({
|
||||||
|
where: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const error: CustomError = new Error('Lowongan tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let imagePath = existing.imagePath;
|
||||||
|
if (req.file) {
|
||||||
|
imagePath = `/uploads/images/${req.file.filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.lowonganMagang.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
posisi: posisi ?? existing.posisi,
|
||||||
|
kelompokPeminatan: kelompok_peminatan ?? existing.kelompokPeminatan,
|
||||||
|
lokasi: lokasi ?? existing.lokasi,
|
||||||
|
jobdesk: jobdesk ?? existing.jobdesk,
|
||||||
|
kualifikasi: kualifikasi ?? existing.kualifikasi,
|
||||||
|
benefit: benefit ?? existing.benefit,
|
||||||
|
durasiAwal: durasi_awal ? new Date(durasi_awal) : existing.durasiAwal,
|
||||||
|
durasiAkhir: durasi_akhir ? new Date(durasi_akhir) : existing.durasiAkhir,
|
||||||
|
statusLowongan: (status_lowongan ?? existing.statusLowongan) === 'DIBUKA' ? 'DIBUKA' : 'DITUTUP',
|
||||||
|
paid: paid ? (paid.toUpperCase() === 'PAID' ? 'PAID' : 'UNPAID') : existing.paid,
|
||||||
|
imagePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Lowongan berhasil diupdate',
|
||||||
|
data: mapToFrontend(updated)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route DELETE /api/lowongan-magang-api/delete/:id
|
||||||
|
* @desc Hapus lowongan magang (khusus admin)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const existing = await prisma.lowonganMagang.findUnique({
|
||||||
|
where: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const error: CustomError = new Error('Lowongan tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.lowonganMagang.delete({
|
||||||
|
where: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Lowongan berhasil dihapus'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||||
|
import prisma from '../lib/prisma';
|
||||||
|
import { upload } from '../middleware/upload';
|
||||||
|
import { CustomError } from '../middleware/errorHandler';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Map data partnership dari DB ke format frontend
|
||||||
|
function mapToFrontend(partner: any) {
|
||||||
|
return {
|
||||||
|
id: partner.id,
|
||||||
|
nama_partner: partner.namaPartner,
|
||||||
|
image_path: partner.imagePath,
|
||||||
|
createdAt: partner.createdAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/partnership-api/get
|
||||||
|
* @desc Dapetin semua data partnerships
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const partners = await prisma.partnership.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: partners.map(mapToFrontend)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/partnership-api/get/:id
|
||||||
|
* @desc Dapetin detail satu partnership
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const partner = await prisma.partnership.findFirst({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!partner) {
|
||||||
|
const error: CustomError = new Error('Partnership tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: [mapToFrontend(partner)] // Kadang list/detail di frontend minta dibungkus array
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/partnership-api/add
|
||||||
|
* @desc Tambah partnership baru (khusus admin ya)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { nama_partner } = req.body;
|
||||||
|
if (!nama_partner) {
|
||||||
|
const error: CustomError = new Error('Nama partnership harus diisi');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.file) {
|
||||||
|
const error: CustomError = new Error('Thumbnail/image harus diunggah');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imagePath = `/uploads/images/${req.file.filename}`;
|
||||||
|
|
||||||
|
const partner = await prisma.partnership.create({
|
||||||
|
data: {
|
||||||
|
namaPartner: nama_partner,
|
||||||
|
imagePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Partnership berhasil ditambahkan',
|
||||||
|
data: mapToFrontend(partner)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route PATCH /api/partnership-api/update/:id
|
||||||
|
* @desc Update data partnership (khusus admin)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { nama_partner } = req.body;
|
||||||
|
|
||||||
|
const existing = await prisma.partnership.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const error: CustomError = new Error('Partnership tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let imagePath = existing.imagePath;
|
||||||
|
if (req.file) {
|
||||||
|
imagePath = `/uploads/images/${req.file.filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.partnership.update({
|
||||||
|
where: { id: parseInt(id, 10) },
|
||||||
|
data: {
|
||||||
|
namaPartner: nama_partner ?? existing.namaPartner,
|
||||||
|
imagePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Partnership berhasil diupdate',
|
||||||
|
data: mapToFrontend(updated)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route DELETE /api/partnership-api/delete/:id
|
||||||
|
* @desc Hapus data partnership (khusus admin)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const existing = await prisma.partnership.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const error: CustomError = new Error('Partnership tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.partnership.delete({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Partnership berhasil dihapus'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { protect, restrictTo, AuthenticatedRequest } from '../middleware/auth';
|
||||||
|
import prisma from '../lib/prisma';
|
||||||
|
import { upload } from '../middleware/upload';
|
||||||
|
import { CustomError } from '../middleware/errorHandler';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Map data research product dari DB ke format frontend
|
||||||
|
function mapToFrontend(product: any) {
|
||||||
|
return {
|
||||||
|
id: product.id,
|
||||||
|
nama_project: product.namaProject,
|
||||||
|
deskripsi: product.deskripsi,
|
||||||
|
link_project: product.linkProject,
|
||||||
|
image_path: product.imagePath,
|
||||||
|
createdAt: product.createdAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/hasil-research-api/get
|
||||||
|
* @desc Dapetin semua hasil riset/products
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/get', async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const products = await prisma.hasilResearch.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: products.map(mapToFrontend)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/hasil-research-api/get/:id
|
||||||
|
* @desc Dapetin detail satu hasil riset/product
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const product = await prisma.hasilResearch.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
const error: CustomError = new Error('Project tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
data: mapToFrontend(product)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/hasil-research-api/add
|
||||||
|
* @desc Tambah hasil riset baru (khusus admin ya)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { nama_project, deskripsi, link_project } = req.body;
|
||||||
|
|
||||||
|
if (!nama_project || !deskripsi || !link_project) {
|
||||||
|
const error: CustomError = new Error('Mohon lengkapi semua field yang wajib diisi');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imagePath = req.file ? `/uploads/images/${req.file.filename}` : '';
|
||||||
|
|
||||||
|
const product = await prisma.hasilResearch.create({
|
||||||
|
data: {
|
||||||
|
namaProject: nama_project,
|
||||||
|
deskripsi,
|
||||||
|
linkProject: link_project,
|
||||||
|
imagePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Research project berhasil ditambahkan',
|
||||||
|
data: mapToFrontend(product)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route PATCH /api/hasil-research-api/update/:id
|
||||||
|
* @desc Update data hasil riset (khusus admin)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { nama_project, deskripsi, link_project } = req.body;
|
||||||
|
|
||||||
|
const existing = await prisma.hasilResearch.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const error: CustomError = new Error('Project tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let imagePath = existing.imagePath;
|
||||||
|
if (req.file) {
|
||||||
|
imagePath = `/uploads/images/${req.file.filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.hasilResearch.update({
|
||||||
|
where: { id: parseInt(id, 10) },
|
||||||
|
data: {
|
||||||
|
namaProject: nama_project ?? existing.namaProject,
|
||||||
|
deskripsi: deskripsi ?? existing.deskripsi,
|
||||||
|
linkProject: link_project ?? existing.linkProject,
|
||||||
|
imagePath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Research project berhasil diupdate',
|
||||||
|
data: mapToFrontend(updated)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route DELETE /api/hasil-research-api/delete/:id
|
||||||
|
* @desc Hapus data hasil riset (khusus admin)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const existing = await prisma.hasilResearch.findUnique({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const error: CustomError = new Error('Project tidak ditemukan');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.hasilResearch.delete({
|
||||||
|
where: { id: parseInt(id, 10) }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: true,
|
||||||
|
message: 'Research project berhasil dihapus'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
%PDF-1.4 mock cv
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
%PDF-1.4 mock portfolio
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
fake-image-content
|
||||||
Reference in New Issue
Block a user