From c138521424372295322f1611c49870d46e59a9c3 Mon Sep 17 00:00:00 2001 From: Muhammad Zhafran Ilham Date: Tue, 2 Jun 2026 08:15:17 +0700 Subject: [PATCH] docs: menambahkan dokumentasi api dengan openapi 3.0.0 --- .env.example | 3 + package-lock.json | 382 ++++++++++++++++++++++++++++++++++++-- package.json | 4 +- src/app.ts | 24 +++ src/docs/swagger.ts | 211 +++++++++++++++++++++ src/routes/auth.ts | 247 +++++++++++++++++++++++- src/routes/faq.ts | 217 ++++++++++++++++++++-- src/routes/feedback.ts | 284 ++++++++++++++++++++++++++-- src/routes/index.ts | 96 +++++++++- src/routes/lamaran.ts | 314 +++++++++++++++++++++++++++++-- src/routes/lowongan.ts | 358 +++++++++++++++++++++++++++++++++-- src/routes/partnership.ts | 253 +++++++++++++++++++++++-- src/routes/research.ts | 255 +++++++++++++++++++++++-- 13 files changed, 2520 insertions(+), 128 deletions(-) create mode 100644 src/docs/swagger.ts diff --git a/.env.example b/.env.example index a7e801a..c5130b8 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,6 @@ DATABASE_URL="mysql://db_username:db_pass@db_host:3306/internify_lms_db" # Authentication Config JWT_SECRET="your_jwt_secret_key_here" JWT_EXPIRES_IN="7d" + +# Swagger +SWAGGER_ENABLE=true \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 75adf77..bb0004e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,8 @@ "jsonwebtoken": "^9.0.3", "morgan": "^1.10.0", "multer": "^2.1.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "xlsx": "^0.18.5" }, "devDependencies": { @@ -35,6 +37,54 @@ "typescript": "^5.4.5" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", + "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz", + "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "14.0.1", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -632,6 +682,13 @@ "@prisma/debug": "6.19.3" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -738,6 +795,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, "node_modules/@types/jsonwebtoken": { "version": "9.0.10", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", @@ -789,7 +852,6 @@ "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -903,6 +965,36 @@ "node": ">=0.8" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -956,6 +1048,12 @@ "dev": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -1146,6 +1244,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, "node_modules/cfb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", @@ -1214,6 +1318,15 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1329,7 +1442,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1402,6 +1514,18 @@ "node": ">=0.3.1" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -1575,6 +1699,15 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1660,6 +1793,28 @@ "node": ">=8.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1695,7 +1850,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -2036,7 +2190,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/jackspeak": { @@ -2065,6 +2218,34 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -2150,6 +2331,12 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -2269,7 +2456,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -2451,11 +2637,17 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parseurl": { @@ -2481,7 +2673,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2563,7 +2754,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.19.3", "@prisma/engines": "6.19.3" @@ -2691,6 +2881,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -2822,7 +3021,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -2835,7 +3033,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2917,7 +3114,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -3122,6 +3318,159 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-jsdoc": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.3.0.tgz", + "integrity": "sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^12.1.0", + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "11.1.0", + "lodash.mergewith": "^4.6.2", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/swagger-jsdoc/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/swagger-jsdoc/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/swagger-jsdoc/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.6", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", + "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tinyexec": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz", @@ -3398,7 +3747,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3458,7 +3806,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -3624,6 +3971,15 @@ "node": ">=0.4" } }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index d1b18ed..a0d1041 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "jsonwebtoken": "^9.0.3", "morgan": "^1.10.0", "multer": "^2.1.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "xlsx": "^0.18.5" }, "devDependencies": { @@ -42,4 +44,4 @@ "tsx": "^4.22.3", "typescript": "^5.4.5" } -} +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 133caca..d19efaf 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,7 @@ import helmet from 'helmet'; import morgan from 'morgan'; import routes from './routes'; import { errorHandler, CustomError } from './middleware/errorHandler'; +import { setupSwaggerDocs } from './docs/swagger'; const app = express(); @@ -25,6 +26,27 @@ app.use('/api/uploads', express.static(path.join(__dirname, '../uploads'))); app.use(express.json()); app.use(express.urlencoded({ extended: true })); +/** + * @swagger + * /: + * get: + * summary: Endpoint root API + * description: Endpoint dasar. + * responses: + * 200: + * description: Informasi umum API berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Welcome to the Internify API + * documentation: + * type: string + * example: Check /api/health untuk status + */ app.get('/', (_req: Request, res: Response) => { res.status(200).json({ message: 'Welcome to the Internify API', @@ -34,6 +56,8 @@ app.get('/', (_req: Request, res: Response) => { app.use('/api', routes); +setupSwaggerDocs(app); + app.use((req: Request, _res: Response, next: NextFunction) => { const error: CustomError = new Error(`Not Found - ${req.originalUrl}`); error.statusCode = 404; diff --git a/src/docs/swagger.ts b/src/docs/swagger.ts new file mode 100644 index 0000000..fadd7bd --- /dev/null +++ b/src/docs/swagger.ts @@ -0,0 +1,211 @@ +import path from 'path'; +import type { Application } from 'express'; + +const swaggerJsdoc = require('swagger-jsdoc'); +const swaggerUi = require('swagger-ui-express'); + +const port = process.env.PORT || '5000'; + +const swaggerOptions = { + definition: { + openapi: '3.0.0', + info: { + title: 'Internify LMS API - HUMIC Engineering', + version: '1.0.0', + description: 'Dokumentasi REST API Internify LMS untuk autentikasi, pengelolaan lowongan magang, lamaran, dan konten platform.', + contact: { + name: 'HUMIC Engineering', + url: 'https://humic.telkomuniversity.ac.id/' + } + }, + servers: [ + { + url: process.env.NODE_ENV === 'production' + ? 'https://your-production-domain.example.com' + : `http://localhost:${port}`, + description: process.env.NODE_ENV === 'production' + ? 'Prod' + : 'Dev' + } + ], + tags: [ + { + name: 'Auth', + description: 'Endpoint autentikasi untuk login admin/peserta dan profil pengguna.' + }, + { + name: 'Admin', + description: 'Endpoint utilitas dan pengelolaan sistem yang terkait admin.' + }, + { + name: 'Batch', + description: 'Endpoint pengelolaan batch/angkatan magang.' + }, + { + name: 'Lowongan Magang', + description: 'Endpoint pengelolaan lowongan magang.' + }, + { + name: 'Lamaran Magang', + description: 'Endpoint pengelolaan lamaran magang.' + }, + { + name: 'Mahasiswa', + description: 'Endpoint pengelolaan data mahasiswa/peserta.' + }, + { + name: 'Partnership', + description: 'Endpoint pengelolaan data mitra/partnership.' + }, + { + name: 'Hasil Research', + description: 'Endpoint pengelolaan hasil research/proyek.' + }, + { + name: 'FAQ', + description: 'Endpoint pengelolaan FAQ (pertanyaan yang sering diajukan).' + }, + { + name: 'Feedback', + description: 'Endpoint pengelolaan feedback/testimoni peserta.' + } + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'Token JWT yang diperoleh dari endpoint /api/auth/login' + } + }, + responses: { + UnauthorizedError: { + description: 'Token akses tidak ada atau tidak valid', + content: { + 'application/json': { + examples: { + tokenMissing: { + value: { + status: 'error', + statusCode: 401, + message: 'Not authorized to access this route, token is missing' + } + }, + tokenInvalid: { + value: { + status: 'error', + statusCode: 401, + message: 'Not authorized to access this route, token is invalid or expired' + } + }, + userNotExist: { + value: { + status: 'error', + statusCode: 401, + message: 'User belonging to this token no longer exists' + } + } + } + } + } + }, + ForbiddenError: { + description: 'Pengguna tidak memiliki izin untuk mengakses resource ini', + content: { + 'application/json': { + example: { + status: 'error', + statusCode: 403, + message: 'You do not have permission to perform this action' + } + } + } + }, + NotFoundError: { + description: 'Resource yang diminta tidak ditemukan', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + status: { + type: 'string', + example: 'error' + }, + statusCode: { + type: 'integer', + example: 404 + }, + message: { + type: 'string', + example: 'Not Found - /api/endpoint-yang-tidak-ada' + } + } + } + } + } + }, + ValidationError: { + description: 'Validasi request gagal', + content: { + 'application/json': { + example: { + status: 'error', + statusCode: 400, + message: 'Mohon lengkapi semua field wajib yang diperlukan' + } + } + } + }, + InternalServerError: { + description: 'Terjadi kesalahan pada server internal', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + status: { + type: 'string', + example: 'error' + }, + statusCode: { + type: 'integer', + example: 500 + }, + message: { + type: 'string', + example: 'Internal Server Error' + } + } + } + } + } + } + } + } + }, + apis: [ + path.join(__dirname, '../app.ts'), + path.join(__dirname, '../app.js'), + path.join(__dirname, '../routes/*.ts'), + path.join(__dirname, '../controllers/*.ts'), + path.join(__dirname, '../routes/*.js'), + path.join(__dirname, '../controllers/*.js') + ] +}; + +const swaggerSpec = swaggerJsdoc(swaggerOptions); + +export const setupSwaggerDocs = (app: Application) => { + const swaggerEnabledValue = process.env.SWAGGER_ENABLE ?? process.env.SWAGGER_ENABLE ?? 'true'; + const isSwaggerEnabled = swaggerEnabledValue.toLowerCase() === 'true'; + if (!isSwaggerEnabled) { + return; + } + + app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + console.log('Swagger UI available at /api/docs'); +}; + +export default setupSwaggerDocs; diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 61d3164..6b8d6cb 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -16,9 +16,110 @@ const generateToken = (payload: { id: string; email: string; role: string }) => }; /** - * @route POST /api/auth/register - * @desc Daftarin user baru dgn role STUDENT (Mahasiswa) - * @access Public + * @swagger + * /api/auth/register: + * post: + * summary: Registrasi akun mahasiswa baru + * description: Membuat akun pengguna baru dengan role `STUDENT` beserta profil mahasiswa. + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * - namaDepan + * - kontak + * - jurusan + * - universitas + * - negara + * properties: + * email: + * type: string + * example: student@example.com + * password: + * type: string + * example: Student123! + * namaDepan: + * type: string + * example: Alya + * namaBelakang: + * type: string + * example: Putri + * kontak: + * type: string + * example: 08123456789 + * jurusan: + * type: string + * example: Informatika + * universitas: + * type: string + * example: Telkom University + * negara: + * type: string + * example: Indonesia + * cvPath: + * type: string + * example: /uploads/docs/cv.pdf + * portofolioPath: + * type: string + * example: /uploads/docs/portofolio.pdf + * motivasi: + * type: string + * example: Saya tertarik mengikuti program ini. + * relevantSkills: + * type: string + * example: React, Node.js + * responses: + * 201: + * description: Registrasi mahasiswa berhasil + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: Registrasi mahasiswa berhasil + * data: + * type: object + * properties: + * id: + * type: string + * example: cku3xq2v0000xk8w7a1b2c3d4 + * email: + * type: string + * example: student@example.com + * role: + * type: string + * example: STUDENT + * createdAt: + * type: string + * format: date-time + * example: 2026-06-02T10:00:00.000Z + * 400: + * description: Validasi registrasi gagal + * content: + * application/json: + * examples: + * field_wajib_kosong: + * value: + * status: error + * statusCode: 400 + * message: Mohon lengkapi semua field wajib yang diperlukan + * email_sudah_terdaftar: + * value: + * status: error + * statusCode: 400 + * message: Email sudah terdaftar + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.post('/register', async (req: Request, res: Response, next: NextFunction) => { try { @@ -100,9 +201,78 @@ router.post('/register', async (req: Request, res: Response, next: NextFunction) }); /** - * @route POST /api/auth/login - * @desc Autentikasi user biar dapet token - * @access Public + * @swagger + * /api/auth/login: + * post: + * summary: Login pengguna dan mendapatkan token JWT + * description: Melakukan autentikasi pengguna (admin/mahasiswa) dan mengembalikan token akses. + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * example: admin@internify.com + * password: + * type: string + * example: AdminInternify123! + * responses: + * 200: + * description: Login berhasil + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * token: + * type: string + * example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + * user: + * type: object + * properties: + * id: + * type: string + * example: cku3xq2v0000xk8w7a1b2c3d4 + * email: + * type: string + * example: admin@internify.com + * role: + * type: string + * example: ADMIN + * profile: + * type: object + * description: Data profil admin atau mahasiswa + * 401: + * description: Autentikasi gagal + * content: + * application/json: + * example: + * status: error + * statusCode: 401 + * message: Email atau password salah + * 400: + * description: Validasi login gagal + * content: + * application/json: + * example: + * status: error + * statusCode: 400 + * message: Mohon masukkan email dan password + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.post('/login', async (req: Request, res: Response, next: NextFunction) => { try { @@ -161,9 +331,68 @@ router.post('/login', async (req: Request, res: Response, next: NextFunction) => }); /** - * @route GET /api/auth/me - * @desc Dapetin info ringkas user yg login buat navbar - * @access Private + * @swagger + * /api/auth/me: + * get: + * summary: Ambil profil pengguna yang sedang login + * description: Mengembalikan informasi ringkas user berdasarkan token JWT aktif. + * tags: [Auth] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Profil pengguna berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: object + * properties: + * id: + * type: string + * example: cku3xq2v0000xk8w7a1b2c3d4 + * email: + * type: string + * example: admin@internify.com + * role: + * type: string + * example: ADMIN + * nama_depan: + * type: string + * example: Super + * 401: + * description: Token tidak valid atau tidak tersedia + * content: + * application/json: + * examples: + * token_missing: + * value: + * status: error + * statusCode: 401 + * message: Not authorized to access this route, token is missing + * token_invalid: + * value: + * status: error + * statusCode: 401 + * message: Not authorized to access this route, token is invalid or expired + * user_token_tidak_ada: + * value: + * status: error + * statusCode: 401 + * message: User belonging to this token no longer exists + * 404: + * description: Pengguna tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: User not found */ router.get('/me', protect, async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { diff --git a/src/routes/faq.ts b/src/routes/faq.ts index 1d4537a..43d3390 100644 --- a/src/routes/faq.ts +++ b/src/routes/faq.ts @@ -16,9 +16,29 @@ function mapToFrontend(faq: any) { } /** - * @route GET /api/faq-api/get - * @desc Dapetin semua list FAQ - * @access Public + * @swagger + * /api/faq-api/get: + * get: + * summary: Ambil seluruh FAQ + * description: Mengembalikan daftar semua pertanyaan dan jawaban FAQ. + * tags: [FAQ] + * responses: + * 200: + * description: Data FAQ berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { try { @@ -35,9 +55,42 @@ router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { }); /** - * @route GET /api/faq-api/get/:id - * @desc Dapetin detail satu FAQ - * @access Public + * @swagger + * /api/faq-api/get/{id}: + * get: + * summary: Ambil detail FAQ berdasarkan ID + * description: Mengembalikan detail satu item FAQ. + * tags: [FAQ] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID FAQ + * responses: + * 200: + * description: Detail FAQ berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: object + * 404: + * description: FAQ tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: FAQ tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => { try { @@ -62,9 +115,58 @@ router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) = }); /** - * @route POST /api/faq-api/add - * @desc Tambah FAQ baru (khusus admin ya) - * @access Private (Admin) + * @swagger + * /api/faq-api/add: + * post: + * summary: Tambah FAQ baru + * description: Menambahkan item FAQ baru. Hanya dapat diakses oleh admin. + * tags: [FAQ] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - pertanyaan + * - jawaban + * properties: + * pertanyaan: + * type: string + * example: Apakah magang ini bisa remote? + * jawaban: + * type: string + * example: Ya, beberapa posisi mendukung skema remote. + * responses: + * 201: + * description: FAQ berhasil ditambahkan + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: FAQ berhasil ditambahkan + * 400: + * description: Validasi FAQ gagal + * content: + * application/json: + * example: + * status: error + * statusCode: 400 + * message: Pertanyaan dan jawaban harus diisi + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.post('/add', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -94,9 +196,60 @@ router.post('/add', protect, restrictTo('ADMIN'), async (req: AuthenticatedReque }); /** - * @route PATCH /api/faq-api/update/:id - * @desc Update data FAQ (khusus admin) - * @access Private (Admin) + * @swagger + * /api/faq-api/update/{id}: + * patch: + * summary: Perbarui FAQ + * description: Memperbarui item FAQ berdasarkan ID. Hanya dapat diakses oleh admin. + * tags: [FAQ] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID FAQ + * requestBody: + * required: false + * content: + * application/json: + * schema: + * type: object + * properties: + * pertanyaan: + * type: string + * jawaban: + * type: string + * responses: + * 200: + * description: FAQ berhasil diperbarui + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: FAQ berhasil diupdate + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * description: FAQ tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: FAQ tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -132,9 +285,43 @@ router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: Authentica }); /** - * @route DELETE /api/faq-api/delete/:id - * @desc Hapus data FAQ (khusus admin) - * @access Private (Admin) + * @swagger + * /api/faq-api/delete/{id}: + * delete: + * summary: Hapus FAQ + * description: Menghapus item FAQ berdasarkan ID. Hanya dapat diakses oleh admin. + * tags: [FAQ] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID FAQ + * responses: + * 200: + * description: FAQ berhasil dihapus + * content: + * application/json: + * example: + * status: true + * message: FAQ berhasil dihapus + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * description: FAQ tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: FAQ tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { diff --git a/src/routes/feedback.ts b/src/routes/feedback.ts index 9c8f5b9..1000e93 100644 --- a/src/routes/feedback.ts +++ b/src/routes/feedback.ts @@ -22,9 +22,29 @@ function mapToFrontend(feedback: any) { } /** - * @route GET /api/feedback-api/get - * @desc Dapetin semua feedback dari mahasiswa - * @access Public + * @swagger + * /api/feedback-api/get: + * get: + * summary: Ambil semua feedback + * description: Mengembalikan daftar seluruh feedback/testimoni peserta. + * tags: [Feedback] + * responses: + * 200: + * description: Data feedback berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { try { @@ -41,9 +61,42 @@ router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { }); /** - * @route GET /api/feedback-api/get/:id - * @desc Dapetin detail satu feedback - * @access Public + * @swagger + * /api/feedback-api/get/{id}: + * get: + * summary: Ambil detail feedback berdasarkan ID + * description: Mengembalikan detail satu feedback/testimoni. + * tags: [Feedback] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID feedback + * responses: + * 200: + * description: Detail feedback berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: object + * 404: + * description: Feedback tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Feedback tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => { try { @@ -68,9 +121,95 @@ router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) = }); /** - * @route POST /api/feedback-api/add - * @desc Tambah feedback baru (bisa admin / student) - * @access Private (Admin/Student) + * @swagger + * /api/feedback-api/add: + * post: + * summary: Tambah feedback baru + * description: Menambahkan feedback/testimoni baru. Dapat diakses user yang sudah login. + * tags: [Feedback] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: + * - nama + * - universitas + * - posisi + * - batch + * - tahun + * - pesan + * properties: + * nama: + * type: string + * example: Alya Putri + * universitas: + * type: string + * example: Telkom University + * posisi: + * type: string + * example: Frontend Developer Intern + * batch: + * type: integer + * example: 3 + * tahun: + * type: integer + * example: 2026 + * pesan: + * type: string + * example: Pengalaman magang sangat membantu perkembangan skill saya. + * image: + * type: string + * format: binary + * responses: + * 201: + * description: Feedback berhasil ditambahkan + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: Feedback berhasil ditambahkan + * 400: + * description: Validasi input feedback gagal + * content: + * application/json: + * examples: + * field_wajib: + * value: + * status: error + * statusCode: 400 + * message: Mohon lengkapi semua field yang wajib diisi + * angka_tidak_valid: + * value: + * status: error + * statusCode: 400 + * message: Batch dan tahun harus berupa angka + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 500: + * description: Terjadi kesalahan server (termasuk validasi file upload) + * content: + * application/json: + * examples: + * file_bukan_gambar: + * value: + * status: error + * statusCode: 500 + * message: File upload harus berupa gambar! + * internal: + * value: + * status: error + * statusCode: 500 + * message: Internal Server Error */ router.post('/add', protect, upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -116,9 +255,90 @@ router.post('/add', protect, upload.single('image'), async (req: AuthenticatedRe }); /** - * @route PATCH /api/feedback-api/update/:id - * @desc Update data feedback (bisa admin / student) - * @access Private (Admin/Student) + * @swagger + * /api/feedback-api/update/{id}: + * patch: + * summary: Perbarui feedback + * description: Memperbarui data feedback berdasarkan ID. Dapat diakses user yang sudah login. + * tags: [Feedback] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID feedback + * requestBody: + * required: false + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * nama: + * type: string + * universitas: + * type: string + * posisi: + * type: string + * batch: + * type: integer + * tahun: + * type: integer + * pesan: + * type: string + * image: + * type: string + * format: binary + * responses: + * 200: + * description: Feedback berhasil diperbarui + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: Feedback berhasil diupdate + * 400: + * description: Validasi data feedback gagal + * content: + * application/json: + * example: + * status: error + * statusCode: 400 + * message: Batch dan tahun harus berupa angka + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 404: + * description: Feedback tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Feedback tidak ditemukan + * 500: + * description: Terjadi kesalahan server (termasuk validasi file upload) + * content: + * application/json: + * examples: + * file_bukan_gambar: + * value: + * status: error + * statusCode: 500 + * message: File upload harus berupa gambar! + * internal: + * value: + * status: error + * statusCode: 500 + * message: Internal Server Error */ router.patch('/update/:id', protect, upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -173,9 +393,43 @@ router.patch('/update/:id', protect, upload.single('image'), async (req: Authent }); /** - * @route DELETE /api/feedback-api/delete/:id - * @desc Hapus feedback (khusus admin) - * @access Private (Admin) + * @swagger + * /api/feedback-api/delete/{id}: + * delete: + * summary: Hapus feedback + * description: Menghapus data feedback berdasarkan ID. Hanya dapat diakses oleh admin. + * tags: [Feedback] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID feedback + * responses: + * 200: + * description: Feedback berhasil dihapus + * content: + * application/json: + * example: + * status: true + * message: Feedback berhasil dihapus + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * description: Feedback tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Feedback tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { diff --git a/src/routes/index.ts b/src/routes/index.ts index 6941a6f..2a6f3ef 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -20,9 +20,30 @@ router.use('/feedback-api', feedbackRouter); router.use('/faq-api', faqRouter); /** - * @route GET /api/health - * @desc Endpoint untuk memeriksa kesehatan/status server - * @access Public + * @swagger + * /api/health: + * get: + * summary: Cek status kesehatan server + * description: Digunakan untuk memastikan API berjalan normal. + * tags: [Admin] + * responses: + * 200: + * description: Server berjalan dengan baik + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: OK + * message: + * type: string + * example: Server is running smoothly + * timestamp: + * type: string + * format: date-time + * example: 2026-06-02T10:00:00.000Z */ router.get('/health', (_req: Request, res: Response) => { res.status(200).json({ @@ -33,9 +54,46 @@ router.get('/health', (_req: Request, res: Response) => { }); /** - * @route GET /api/users - * @desc Mendapatkan daftar semua user dari database menggunakan Prisma - * @access Public + * @swagger + * /api/users: + * get: + * summary: Ambil daftar seluruh pengguna + * description: Mengembalikan seluruh data user dasar dari database. + * tags: [Admin] + * responses: + * 200: + * description: Daftar pengguna berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * count: + * type: integer + * example: 2 + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * example: cku3xq2v0000xk8w7a1b2c3d4 + * email: + * type: string + * example: admin@internify.com + * role: + * type: string + * example: ADMIN + * createdAt: + * type: string + * format: date-time + * example: 2026-06-02T10:00:00.000Z + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/users', async (_req: Request, res: Response, next: NextFunction) => { try { @@ -58,9 +116,29 @@ router.get('/users', async (_req: Request, res: Response, next: NextFunction) => }); /** - * @route GET /api/error-test - * @desc Mensimulasikan error server internal untuk memverifikasi middleware penanganan error global - * @access Public + * @swagger + * /api/error-test: + * get: + * summary: Simulasi error server untuk pengujian + * description: Endpoint utilitas untuk menguji middleware penanganan error global. + * tags: [Admin] + * responses: + * 500: + * description: Error simulasi berhasil dipicu + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * statusCode: + * type: integer + * example: 500 + * message: + * type: string + * example: This is a test error to verify our custom global error handler! */ router.get('/error-test', (_req: Request, _res: Response, next: NextFunction) => { try { diff --git a/src/routes/lamaran.ts b/src/routes/lamaran.ts index 4bbbeef..56a3bae 100644 --- a/src/routes/lamaran.ts +++ b/src/routes/lamaran.ts @@ -39,9 +39,124 @@ function mapToFrontend(lamaran: any) { } /** - * @route POST /api/lamaran-magang-api/add/:lowonganId - * @desc Kirim pendaftaran magang baru - * @access Public + * @swagger + * /api/lamaran-magang-api/add/{lowonganId}: + * post: + * summary: Kirim lamaran magang baru + * description: Mengirim pendaftaran magang ke lowongan tertentu beserta CV dan portofolio (PDF). + * tags: [Lamaran Magang] + * parameters: + * - in: path + * name: lowonganId + * required: true + * schema: + * type: string + * description: ID lowongan magang + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: + * - nama_depan + * - email + * - kontak + * - universitas + * - negara + * - jurusan + * - motivasi + * - cv + * - portofolio + * properties: + * nama_depan: + * type: string + * example: Alya + * nama_belakang: + * type: string + * example: Putri + * email: + * type: string + * example: alya@example.com + * kontak: + * type: string + * example: 081234567890 + * universitas: + * type: string + * example: Telkom University + * negara: + * type: string + * example: Indonesia + * jurusan: + * type: string + * example: Informatika + * batch: + * type: integer + * example: 3 + * motivasi: + * type: string + * example: Saya ingin belajar langsung dari tim engineering. + * relevant_skills: + * type: string + * example: React, TypeScript, Node.js + * cv: + * type: string + * format: binary + * portofolio: + * type: string + * format: binary + * responses: + * 201: + * description: Lamaran berhasil dikirim + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: Pendaftaran magang berhasil dikirim + * 400: + * description: Validasi pengajuan lamaran gagal + * content: + * application/json: + * examples: + * field_wajib: + * value: + * status: error + * statusCode: 400 + * message: Mohon lengkapi semua field wajib yang diperlukan + * dokumen_wajib: + * value: + * status: error + * statusCode: 400 + * message: CV dan Portofolio wajib diunggah! + * 404: + * description: Lowongan magang tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Lowongan magang tidak ditemukan + * 500: + * description: Terjadi kesalahan server (termasuk validasi tipe file upload) + * content: + * application/json: + * examples: + * tipe_file_pdf: + * value: + * status: error + * statusCode: 500 + * message: CV dan Portofolio harus dalam format PDF! + * internal: + * value: + * status: error + * statusCode: 500 + * message: Internal Server Error */ router.post( '/add/:lowonganId', @@ -177,9 +292,35 @@ router.post( ); /** - * @route GET /api/lamaran-magang-api/get - * @desc Dapetin semua data pelamar (admin aja ya) - * @access Private (Admin) + * @swagger + * /api/lamaran-magang-api/get: + * get: + * summary: Ambil seluruh data lamaran + * description: Mengambil daftar seluruh lamaran magang. Hanya dapat diakses oleh admin. + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Data lamaran berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -202,9 +343,48 @@ router.get('/get', protect, restrictTo('ADMIN'), async (_req: AuthenticatedReque }); /** - * @route GET /api/lamaran-magang-api/get/:id - * @desc Dapetin detail satu lamaran berdasarkan id - * @access Private (Admin) + * @swagger + * /api/lamaran-magang-api/get/{id}: + * get: + * summary: Ambil detail lamaran berdasarkan ID + * description: Mengambil detail satu lamaran magang. Hanya dapat diakses oleh admin. + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID lamaran magang + * responses: + * 200: + * description: Detail lamaran berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: object + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * description: Lamaran tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Lamaran tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -235,9 +415,69 @@ router.get('/get/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRe }); /** - * @route PATCH /api/lamaran-magang-api/update/:id - * @desc Ganti status lamaran (diterima, ditolak, dll) - * @access Private (Admin) + * @swagger + * /api/lamaran-magang-api/update/{id}: + * patch: + * summary: Perbarui status lamaran + * description: Mengubah status lamaran menjadi `DIPROSES`, `DITERIMA`, atau `DITOLAK`. Hanya admin. + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID lamaran magang + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - status + * properties: + * status: + * type: string + * enum: [DIPROSES, DITERIMA, DITOLAK] + * example: DITERIMA + * responses: + * 200: + * description: Status lamaran berhasil diperbarui + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: Status lamaran berhasil diupdate + * 400: + * description: Validasi status lamaran gagal + * content: + * application/json: + * examples: + * status_kosong: + * value: + * status: error + * statusCode: 400 + * message: Mohon masukkan status baru + * status_tidak_valid: + * value: + * status: error + * statusCode: 400 + * message: Status tidak valid + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -281,9 +521,28 @@ router.patch('/update/:id', protect, restrictTo('ADMIN'), async (req: Authentica }); /** - * @route DELETE /api/lamaran-magang-api/delete - * @desc Hapus semua data pelamar - * @access Private (Admin) + * @swagger + * /api/lamaran-magang-api/delete: + * delete: + * summary: Hapus seluruh data lamaran + * description: Menghapus seluruh data lamaran magang. Hanya dapat diakses oleh admin. + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Seluruh data pelamar berhasil dihapus + * content: + * application/json: + * example: + * status: true + * message: Seluruh data pelamar berhasil dihapus + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.delete('/delete', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -298,9 +557,28 @@ router.delete('/delete', protect, restrictTo('ADMIN'), async (_req: Authenticate }); /** - * @route GET /api/lamaran-magang-api/export - * @desc Ekspor data pelamar ke file Excel (.xlsx) - * @access Private (Admin) + * @swagger + * /api/lamaran-magang-api/export: + * get: + * summary: Ekspor data lamaran ke file Excel + * description: Mengunduh data lamaran dalam format `.xlsx`. Hanya dapat diakses oleh admin. + * tags: [Lamaran Magang] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: File Excel berhasil dibuat + * content: + * application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + * schema: + * type: string + * format: binary + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/export', protect, restrictTo('ADMIN'), async (_req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { diff --git a/src/routes/lowongan.ts b/src/routes/lowongan.ts index a852ad4..5728fe7 100644 --- a/src/routes/lowongan.ts +++ b/src/routes/lowongan.ts @@ -42,9 +42,42 @@ function mapToFrontend(lowongan: any) { } /** - * @route GET /api/lowongan-magang-api/get - * @desc Ambil semua daftar lowongan magang nih - * @access Public + * @swagger + * /api/lowongan-magang-api/get: + * get: + * summary: Ambil seluruh lowongan magang + * description: Mengembalikan daftar semua lowongan magang yang tersedia. + * tags: [Lowongan Magang] + * responses: + * 200: + * description: Daftar lowongan berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * example: lowongan-frontend + * posisi: + * type: string + * example: Frontend Developer Intern + * kelompok_peminatan: + * type: string + * example: Web Engineering + * status_lowongan: + * type: string + * example: DIBUKA + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { try { @@ -61,9 +94,30 @@ router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { }); /** - * @route GET /api/lowongan-magang-api/get/kelompok-all - * @desc Ambil semua kelompok peminatan yg unik - * @access Public + * @swagger + * /api/lowongan-magang-api/get/kelompok-all: + * get: + * summary: Ambil semua kelompok peminatan unik + * description: Mengembalikan daftar kategori peminatan tanpa duplikasi. + * tags: [Lowongan Magang] + * responses: + * 200: + * description: Daftar kelompok peminatan berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: string + * example: ["Web Engineering", "Design & Creative"] + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get/kelompok-all', async (_req: Request, res: Response, next: NextFunction) => { try { @@ -82,9 +136,52 @@ router.get('/get/kelompok-all', async (_req: Request, res: Response, next: NextF }); /** - * @route GET /api/lowongan-magang-api/get/id/:id - * @desc Ambil detail lowongan magang berdasarkan id - * @access Public + * @swagger + * /api/lowongan-magang-api/get/id/{id}: + * get: + * summary: Ambil detail lowongan berdasarkan ID + * description: Mengembalikan detail satu lowongan magang berdasarkan parameter `id`. + * tags: [Lowongan Magang] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: ID lowongan + * responses: + * 200: + * description: Detail lowongan berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: object + * properties: + * id: + * type: string + * example: lowongan-frontend + * posisi: + * type: string + * example: Frontend Developer Intern + * kelompok_peminatan: + * type: string + * example: Web Engineering + * 404: + * description: Lowongan tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Lowongan tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get/id/:id', async (req: Request, res: Response, next: NextFunction) => { try { @@ -107,9 +204,105 @@ router.get('/get/id/:id', async (req: Request, res: Response, next: NextFunction }); /** - * @route POST /api/lowongan-magang-api/add - * @desc Tambah lowongan magang baru (khusus admin ya) - * @access Private (Admin) + * @swagger + * /api/lowongan-magang-api/add: + * post: + * summary: Tambah lowongan magang baru + * description: Membuat lowongan magang baru. Hanya dapat diakses oleh admin. + * tags: [Lowongan Magang] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: + * - posisi + * - kelompok_peminatan + * - lokasi + * - jobdesk + * - durasi_awal + * - durasi_akhir + * - paid + * properties: + * posisi: + * type: string + * example: Frontend Developer Intern + * kelompok_peminatan: + * type: string + * example: Web Engineering + * lokasi: + * type: string + * example: Remote + * jobdesk: + * type: string + * example: Mengembangkan antarmuka responsif menggunakan React. + * kualifikasi: + * type: string + * example: Memahami React JS, ES6 JavaScript, HTML, CSS. + * benefit: + * type: string + * example: Sertifikat magang resmi, jam kerja fleksibel. + * durasi_awal: + * type: string + * format: date + * example: 2026-06-01 + * durasi_akhir: + * type: string + * format: date + * example: 2026-09-01 + * paid: + * type: string + * enum: [PAID, UNPAID] + * example: PAID + * image: + * type: string + * format: binary + * responses: + * 201: + * description: Lowongan berhasil ditambahkan + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: Internship lowongan berhasil ditambahkan + * data: + * type: object + * 400: + * description: Validasi input lowongan gagal + * content: + * application/json: + * example: + * status: error + * statusCode: 400 + * message: Mohon lengkapi semua field yang wajib diisi + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 500: + * description: Terjadi kesalahan server (termasuk validasi file upload) + * content: + * application/json: + * examples: + * file_bukan_gambar: + * value: + * status: error + * statusCode: 500 + * message: File upload harus berupa gambar! + * internal: + * value: + * status: error + * statusCode: 500 + * message: Internal Server Error */ router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -166,9 +359,98 @@ router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async }); /** - * @route PATCH /api/lowongan-magang-api/update/:id - * @desc Update data lowongan magang (khusus admin) - * @access Private (Admin) + * @swagger + * /api/lowongan-magang-api/update/{id}: + * patch: + * summary: Perbarui data lowongan magang + * description: Memperbarui data lowongan berdasarkan ID. Hanya dapat diakses oleh admin. + * tags: [Lowongan Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: ID lowongan + * requestBody: + * required: false + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * posisi: + * type: string + * kelompok_peminatan: + * type: string + * lokasi: + * type: string + * jobdesk: + * type: string + * kualifikasi: + * type: string + * benefit: + * type: string + * durasi_awal: + * type: string + * format: date + * durasi_akhir: + * type: string + * format: date + * paid: + * type: string + * enum: [PAID, UNPAID] + * status_lowongan: + * type: string + * enum: [DIBUKA, DITUTUP] + * image: + * type: string + * format: binary + * responses: + * 200: + * description: Lowongan berhasil diperbarui + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: Lowongan berhasil diupdate + * data: + * type: object + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * description: Lowongan tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Lowongan tidak ditemukan + * 500: + * description: Terjadi kesalahan server (termasuk validasi file upload) + * content: + * application/json: + * examples: + * file_bukan_gambar: + * value: + * status: error + * statusCode: 500 + * message: File upload harus berupa gambar! + * internal: + * value: + * status: error + * statusCode: 500 + * message: Internal Server Error */ router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -229,9 +511,49 @@ router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image') }); /** - * @route DELETE /api/lowongan-magang-api/delete/:id - * @desc Hapus lowongan magang (khusus admin) - * @access Private (Admin) + * @swagger + * /api/lowongan-magang-api/delete/{id}: + * delete: + * summary: Hapus lowongan magang + * description: Menghapus data lowongan berdasarkan ID. Hanya dapat diakses oleh admin. + * tags: [Lowongan Magang] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: ID lowongan + * responses: + * 200: + * description: Lowongan berhasil dihapus + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: Lowongan berhasil dihapus + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * description: Lowongan tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Lowongan tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { diff --git a/src/routes/partnership.ts b/src/routes/partnership.ts index 2cebd17..0102df8 100644 --- a/src/routes/partnership.ts +++ b/src/routes/partnership.ts @@ -17,9 +17,29 @@ function mapToFrontend(partner: any) { } /** - * @route GET /api/partnership-api/get - * @desc Dapetin semua data partnerships - * @access Public + * @swagger + * /api/partnership-api/get: + * get: + * summary: Ambil seluruh data partnership + * description: Mengembalikan daftar seluruh mitra partnership. + * tags: [Partnership] + * responses: + * 200: + * description: Data partnership berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { try { @@ -36,9 +56,44 @@ router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { }); /** - * @route GET /api/partnership-api/get/:id - * @desc Dapetin detail satu partnership - * @access Public + * @swagger + * /api/partnership-api/get/{id}: + * get: + * summary: Ambil detail partnership berdasarkan ID + * description: Mengembalikan detail satu data partnership. + * tags: [Partnership] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID partnership + * responses: + * 200: + * description: Detail partnership berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * 404: + * description: Partnership tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Partnership tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => { try { @@ -63,9 +118,78 @@ router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) = }); /** - * @route POST /api/partnership-api/add - * @desc Tambah partnership baru (khusus admin ya) - * @access Private (Admin) + * @swagger + * /api/partnership-api/add: + * post: + * summary: Tambah partnership baru + * description: Menambahkan data partnership baru. Hanya dapat diakses oleh admin. + * tags: [Partnership] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: + * - nama_partner + * - image + * properties: + * nama_partner: + * type: string + * example: PT Teknologi Nusantara + * image: + * type: string + * format: binary + * responses: + * 201: + * description: Partnership berhasil ditambahkan + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: Partnership berhasil ditambahkan + * 400: + * description: Validasi input partnership gagal + * content: + * application/json: + * examples: + * nama_wajib: + * value: + * status: error + * statusCode: 400 + * message: Nama partnership harus diisi + * image_wajib: + * value: + * status: error + * statusCode: 400 + * message: Thumbnail/image harus diunggah + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 500: + * description: Terjadi kesalahan server (termasuk validasi file upload) + * content: + * application/json: + * examples: + * file_bukan_gambar: + * value: + * status: error + * statusCode: 500 + * message: File upload harus berupa gambar! + * internal: + * value: + * status: error + * statusCode: 500 + * message: Internal Server Error */ router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -102,9 +226,74 @@ router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async }); /** - * @route PATCH /api/partnership-api/update/:id - * @desc Update data partnership (khusus admin) - * @access Private (Admin) + * @swagger + * /api/partnership-api/update/{id}: + * patch: + * summary: Perbarui data partnership + * description: Memperbarui data partnership berdasarkan ID. Hanya dapat diakses oleh admin. + * tags: [Partnership] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID partnership + * requestBody: + * required: false + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * nama_partner: + * type: string + * image: + * type: string + * format: binary + * responses: + * 200: + * description: Partnership berhasil diperbarui + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: Partnership berhasil diupdate + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * description: Partnership tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Partnership tidak ditemukan + * 500: + * description: Terjadi kesalahan server (termasuk validasi file upload) + * content: + * application/json: + * examples: + * file_bukan_gambar: + * value: + * status: error + * statusCode: 500 + * message: File upload harus berupa gambar! + * internal: + * value: + * status: error + * statusCode: 500 + * message: Internal Server Error */ router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -145,9 +334,43 @@ router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image') }); /** - * @route DELETE /api/partnership-api/delete/:id - * @desc Hapus data partnership (khusus admin) - * @access Private (Admin) + * @swagger + * /api/partnership-api/delete/{id}: + * delete: + * summary: Hapus partnership + * description: Menghapus data partnership berdasarkan ID. Hanya dapat diakses oleh admin. + * tags: [Partnership] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID partnership + * responses: + * 200: + * description: Partnership berhasil dihapus + * content: + * application/json: + * example: + * status: true + * message: Partnership berhasil dihapus + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * description: Partnership tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Partnership tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { diff --git a/src/routes/research.ts b/src/routes/research.ts index e4fa316..70de8c9 100644 --- a/src/routes/research.ts +++ b/src/routes/research.ts @@ -19,9 +19,29 @@ function mapToFrontend(product: any) { } /** - * @route GET /api/hasil-research-api/get - * @desc Dapetin semua hasil riset/products - * @access Public + * @swagger + * /api/hasil-research-api/get: + * get: + * summary: Ambil semua hasil research + * description: Mengembalikan daftar seluruh project hasil research. + * tags: [Hasil Research] + * responses: + * 200: + * description: Data hasil research berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: array + * items: + * type: object + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { try { @@ -38,9 +58,42 @@ router.get('/get', async (_req: Request, res: Response, next: NextFunction) => { }); /** - * @route GET /api/hasil-research-api/get/:id - * @desc Dapetin detail satu hasil riset/product - * @access Public + * @swagger + * /api/hasil-research-api/get/{id}: + * get: + * summary: Ambil detail hasil research berdasarkan ID + * description: Mengembalikan detail satu project hasil research. + * tags: [Hasil Research] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID hasil research + * responses: + * 200: + * description: Detail hasil research berhasil diambil + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * data: + * type: object + * 404: + * description: Project tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Project tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) => { try { @@ -65,9 +118,78 @@ router.get('/get/:id', async (req: Request, res: Response, next: NextFunction) = }); /** - * @route POST /api/hasil-research-api/add - * @desc Tambah hasil riset baru (khusus admin ya) - * @access Private (Admin) + * @swagger + * /api/hasil-research-api/add: + * post: + * summary: Tambah hasil research baru + * description: Menambahkan data project hasil research baru. Hanya dapat diakses oleh admin. + * tags: [Hasil Research] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: + * - nama_project + * - deskripsi + * - link_project + * properties: + * nama_project: + * type: string + * example: Sistem Monitoring Tanaman IoT + * deskripsi: + * type: string + * example: Project monitoring kelembapan tanah berbasis IoT. + * link_project: + * type: string + * example: https://example.com/project/monitoring-tanaman + * image: + * type: string + * format: binary + * responses: + * 201: + * description: Hasil research berhasil ditambahkan + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: Research project berhasil ditambahkan + * 400: + * description: Validasi input hasil research gagal + * content: + * application/json: + * example: + * status: error + * statusCode: 400 + * message: Mohon lengkapi semua field yang wajib diisi + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 500: + * description: Terjadi kesalahan server (termasuk validasi file upload) + * content: + * application/json: + * examples: + * file_bukan_gambar: + * value: + * status: error + * statusCode: 500 + * message: File upload harus berupa gambar! + * internal: + * value: + * status: error + * statusCode: 500 + * message: Internal Server Error */ router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -101,9 +223,78 @@ router.post('/add', protect, restrictTo('ADMIN'), upload.single('image'), async }); /** - * @route PATCH /api/hasil-research-api/update/:id - * @desc Update data hasil riset (khusus admin) - * @access Private (Admin) + * @swagger + * /api/hasil-research-api/update/{id}: + * patch: + * summary: Perbarui hasil research + * description: Memperbarui data hasil research berdasarkan ID. Hanya dapat diakses oleh admin. + * tags: [Hasil Research] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID hasil research + * requestBody: + * required: false + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * nama_project: + * type: string + * deskripsi: + * type: string + * link_project: + * type: string + * image: + * type: string + * format: binary + * responses: + * 200: + * description: Hasil research berhasil diperbarui + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: true + * message: + * type: string + * example: Research project berhasil diupdate + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * description: Project tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Project tidak ditemukan + * 500: + * description: Terjadi kesalahan server (termasuk validasi file upload) + * content: + * application/json: + * examples: + * file_bukan_gambar: + * value: + * status: error + * statusCode: 500 + * message: File upload harus berupa gambar! + * internal: + * value: + * status: error + * statusCode: 500 + * message: Internal Server Error */ router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try { @@ -146,9 +337,43 @@ router.patch('/update/:id', protect, restrictTo('ADMIN'), upload.single('image') }); /** - * @route DELETE /api/hasil-research-api/delete/:id - * @desc Hapus data hasil riset (khusus admin) - * @access Private (Admin) + * @swagger + * /api/hasil-research-api/delete/{id}: + * delete: + * summary: Hapus hasil research + * description: Menghapus data hasil research berdasarkan ID. Hanya dapat diakses oleh admin. + * tags: [Hasil Research] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID hasil research + * responses: + * 200: + * description: Hasil research berhasil dihapus + * content: + * application/json: + * example: + * status: true + * message: Research project berhasil dihapus + * 401: + * $ref: '#/components/responses/UnauthorizedError' + * 403: + * $ref: '#/components/responses/ForbiddenError' + * 404: + * description: Project tidak ditemukan + * content: + * application/json: + * example: + * status: error + * statusCode: 404 + * message: Project tidak ditemukan + * 500: + * $ref: '#/components/responses/InternalServerError' */ router.delete('/delete/:id', protect, restrictTo('ADMIN'), async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { try {