From 6c73549be7c76b018ad984721e9ebdc78d063abc Mon Sep 17 00:00:00 2001 From: Muhamad Dendi Purwanto Date: Tue, 22 Apr 2025 21:02:37 +0700 Subject: [PATCH 1/5] fix --- Dockerfile | 4 +- package-lock.json | 357 ++++++++++++++++-- package.json | 12 +- src/api-docs/openAPIDocumentGenerator.ts | 20 + src/api-docs/openAPIRouter.ts | 2 +- src/auth/auth.module.ts | 17 + src/auth/authRouter.ts | 62 +++ src/auth/jwt.guard.ts | 5 + src/auth/jwt.strategy.ts | 18 + src/common/middleware/jwtMiddleware.ts | 129 +++++++ src/common/middleware/rateLimiter.ts | 2 +- src/index.ts | 0 .../excelGenerator/excelGeneratorModel.ts | 84 +---- .../excelGenerator/excelGeneratorRouter.ts | 235 +++++++++--- src/routes/healthCheck/healthCheckRouter.ts | 33 +- .../powerpointGeneratorRouter.ts | 136 +++++-- .../webPageReader/webPageReaderRouter.ts | 29 +- .../wordGenerator/wordGeneratorRouter.ts | 93 ++++- .../youtubeTranscriptRouter.ts | 30 +- src/server.ts | 45 ++- tsconfig.json | 14 +- 21 files changed, 1100 insertions(+), 227 deletions(-) create mode 100644 src/auth/auth.module.ts create mode 100644 src/auth/authRouter.ts create mode 100644 src/auth/jwt.guard.ts create mode 100644 src/auth/jwt.strategy.ts create mode 100644 src/common/middleware/jwtMiddleware.ts mode change 100755 => 100644 src/index.ts diff --git a/Dockerfile b/Dockerfile index 3a1efbb..7cf4c63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,13 @@ FROM node:current-slim # Create app directory -WORKDIR /usr/src/app +WORKDIR /app # Copy package.json and package-lock.json COPY package*.json ./ # Install app dependencies -RUN npm ci +RUN npm Install # Bundle app source COPY . . diff --git a/package-lock.json b/package-lock.json index cdbcfd8..964771d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,17 @@ "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@mozilla/readability": "^0.5.0", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", "@notionhq/client": "^2.2.15", "@types/got": "^9.6.12", "@types/jsdom": "^21.1.6", + "@types/jsonwebtoken": "^9.0.9", + "@types/passport-jwt": "^4.0.1", "body-parser": "^1.20.2", "cheerio": "^1.0.0-rc.12", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "cors": "^2.8.5", "docx": "^9.1.0", "dotenv": "^16.4.5", @@ -28,14 +34,17 @@ "helmet": "^7.1.0", "http-status-codes": "^2.3.0", "jsdom": "^26.0.0", + "jsonwebtoken": "^9.0.2", "node-cron": "^3.0.3", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "path": "^0.12.7", "pino-http": "^10.4.0", "pptxgenjs": "^3.12.0", "swagger-ui-express": "^5.0.0", "uuidv4": "^6.2.13", "youtube-transcript": "^1.1.0", - "zod": "^3.22.4" + "zod": "^3.24.2" }, "devDependencies": { "@commitlint/cli": "^19.0.3", @@ -97,6 +106,7 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.3.0.tgz", "integrity": "sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==", + "license": "MIT", "dependencies": { "openapi3-ts": "^4.1.2" }, @@ -1278,6 +1288,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@mozilla/readability": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.5.0.tgz", @@ -1286,6 +1306,75 @@ "node": ">=14.0.0" } }, + "node_modules/@nestjs/common": { + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.0.10.tgz", + "integrity": "sha512-pzGXp14KF2Q4CDZGQgPK4l8zEg7i6cNkb+10yc8ZA5K41cLe3ZbWW1YxtY2e/glHauOJwTLSVjH4tiRVtOTizg==", + "license": "MIT", + "peer": true, + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/common/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, + "node_modules/@nestjs/jwt": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", + "integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.7", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/jwt/node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1878,7 +1967,6 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -1888,7 +1976,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -1927,7 +2014,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -1939,7 +2025,6 @@ "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -1965,8 +2050,7 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/jsdom": { "version": "21.1.7", @@ -1978,6 +2062,16 @@ "parse5": "^7.0.0" } }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -1987,8 +2081,13 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" }, "node_modules/@types/node": { "version": "22.10.1", @@ -2032,17 +2131,44 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", - "dev": true + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/semver": { "version": "7.5.8", @@ -2054,7 +2180,6 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -2064,7 +2189,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -2127,6 +2251,12 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" }, + "node_modules/@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", @@ -2978,6 +3108,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3251,6 +3387,23 @@ "node": ">=8" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -4433,6 +4586,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6930,6 +7092,16 @@ "node": ">=8" } }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -7094,6 +7266,28 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -7132,6 +7326,27 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7218,6 +7433,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.20.tgz", + "integrity": "sha512-/ipwAMvtSZRdiQBHqW1qxqeYiBMzncOQLVA+62MWYr7N4m7Q2jqpJ0WgT7zlOEOpyLRSqrMXidbJpC0J77AaKA==", + "license": "MIT" + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -7393,6 +7614,12 @@ "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -7408,11 +7635,23 @@ "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, "node_modules/lodash.isnil": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -7421,8 +7660,7 @@ "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, "node_modules/lodash.isundefined": { "version": "3.0.1", @@ -7447,6 +7685,12 @@ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", @@ -8445,6 +8689,42 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path": { "version": "0.12.7", "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", @@ -8531,6 +8811,11 @@ "node": "*" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9239,6 +9524,13 @@ "node": ">= 0.10" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/registry-auth-token": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.3.tgz", @@ -9640,7 +9932,6 @@ "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, "dependencies": { "tslib": "^2.1.0" } @@ -9703,7 +9994,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -11340,6 +11630,19 @@ "node": ">=0.8.0" } }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/undici": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", @@ -11519,6 +11822,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -12717,9 +13029,10 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index ab5fde9..5827b0c 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "TypingMind Proxy", "main": "index.ts", "scripts": { + "deploy": "./deploy.sh", "dev": "tsx watch --clear-screen=false src/index.ts | pino-pretty", "build": "rimraf dist && tsup", "start": "tsx dist/index.js", @@ -20,11 +21,17 @@ "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@mozilla/readability": "^0.5.0", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", "@notionhq/client": "^2.2.15", "@types/got": "^9.6.12", "@types/jsdom": "^21.1.6", + "@types/jsonwebtoken": "^9.0.9", + "@types/passport-jwt": "^4.0.1", "body-parser": "^1.20.2", "cheerio": "^1.0.0-rc.12", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "cors": "^2.8.5", "docx": "^9.1.0", "dotenv": "^16.4.5", @@ -37,14 +44,17 @@ "helmet": "^7.1.0", "http-status-codes": "^2.3.0", "jsdom": "^26.0.0", + "jsonwebtoken": "^9.0.2", "node-cron": "^3.0.3", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "path": "^0.12.7", "pino-http": "^10.4.0", "pptxgenjs": "^3.12.0", "swagger-ui-express": "^5.0.0", "uuidv4": "^6.2.13", "youtube-transcript": "^1.1.0", - "zod": "^3.22.4" + "zod": "^3.24.2" }, "devDependencies": { "@commitlint/cli": "^19.0.3", diff --git a/src/api-docs/openAPIDocumentGenerator.ts b/src/api-docs/openAPIDocumentGenerator.ts index b430210..24855dd 100644 --- a/src/api-docs/openAPIDocumentGenerator.ts +++ b/src/api-docs/openAPIDocumentGenerator.ts @@ -1,4 +1,6 @@ +// src/api-docs/openAPIDocumentGenerator.ts import { OpenApiGeneratorV3, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { z } from 'zod'; import { excelGeneratorRegistry } from '@/routes/excelGenerator/excelGeneratorRouter'; import { healthCheckRegistry } from '@/routes/healthCheck/healthCheckRouter'; @@ -18,6 +20,19 @@ export function generateOpenAPIDocument() { excelGeneratorRegistry, notionDatabaseRegistry, ]); + + // Tambahkan skema keamanan JWT + registry.registerComponent('securitySchemes', 'bearerAuth', { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }); + + // Contoh skema untuk token (opsional) + const JwtTokenSchema = z.object({ + token: z.string().describe('JWT Authentication Token'), + }); + const generator = new OpenApiGeneratorV3(registry.definitions); return generator.generateDocument({ @@ -30,5 +45,10 @@ export function generateOpenAPIDocument() { description: 'View the raw OpenAPI Specification in JSON format', url: '/swagger.json', }, + security: [ + { + bearerAuth: [], + }, + ], }); } diff --git a/src/api-docs/openAPIRouter.ts b/src/api-docs/openAPIRouter.ts index 9a4d004..f6b8f81 100644 --- a/src/api-docs/openAPIRouter.ts +++ b/src/api-docs/openAPIRouter.ts @@ -12,7 +12,7 @@ export const openAPIRouter: Router = (() => { res.send(openAPIDocument); }); - router.use('/', swaggerUi.serve, swaggerUi.setup(openAPIDocument)); + router.use('/docs-plugin', swaggerUi.serve, swaggerUi.setup(openAPIDocument)); return router; })(); diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..72208d7 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { JwtStrategy } from './jwt.strategy'; + +@Module({ + imports: [ + PassportModule, + JwtModule.register({ + secret: process.env.JWT_SECRET || 'your-secret-key', + signOptions: { expiresIn: '1h' }, + }), + ], + providers: [JwtStrategy], + exports: [JwtModule], +}) +export class AuthModule {} \ No newline at end of file diff --git a/src/auth/authRouter.ts b/src/auth/authRouter.ts new file mode 100644 index 0000000..99f43a7 --- /dev/null +++ b/src/auth/authRouter.ts @@ -0,0 +1,62 @@ +import express from 'express'; +import jwt from 'jsonwebtoken'; +import { z } from 'zod'; + +const router = express.Router(); +const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key_for_development'; + +// Skema validasi login untuk kredensial sementara +const LoginSchema = z.object({ + username: z.string().startsWith('tmp_user_', "Username must be a temporary user"), + password: z.string().startsWith('tmp_pass_', "Password must be a temporary password") +}); + +router.post('/login', (req, res) => { + try { + const { username, password } = req.body; + + // Validasi input untuk kredensial sementara + LoginSchema.parse({ username, password }); + + // Generate ID unik untuk pengguna sementara + const userId = `tmp_${Date.now()}_${Math.random().toString(36).substring(7)}`; + + // Generate token + const token = jwt.sign( + { + id: userId, + username: username, + type: 'temporary' + }, + JWT_SECRET, + { + expiresIn: '1h' + } + ); + + res.json({ + token, + user: { + id: userId, + username: username + } + }); + } catch (error) { + console.error('Temporary Login Error:', error); + + // Tangani kesalahan validasi atau autentikasi + if (error instanceof z.ZodError) { + return res.status(400).json({ + message: 'Invalid temporary credentials', + errors: error.errors + }); + } + + res.status(401).json({ + message: 'Temporary login failed', + error: error instanceof Error ? error.message : 'Unknown error' + }); + } +}); + +export default router; diff --git a/src/auth/jwt.guard.ts b/src/auth/jwt.guard.ts new file mode 100644 index 0000000..18588a5 --- /dev/null +++ b/src/auth/jwt.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} \ No newline at end of file diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts new file mode 100644 index 0000000..3f5f602 --- /dev/null +++ b/src/auth/jwt.strategy.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: process.env.JWT_SECRET || 'your-secret-key', + }); + } + + async validate(payload: any) { + return payload; + } +} \ No newline at end of file diff --git a/src/common/middleware/jwtMiddleware.ts b/src/common/middleware/jwtMiddleware.ts new file mode 100644 index 0000000..442e1ae --- /dev/null +++ b/src/common/middleware/jwtMiddleware.ts @@ -0,0 +1,129 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import { StatusCodes } from 'http-status-codes'; +import path from 'path'; + +import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; +import { handleServiceResponse } from '@/common/utils/httpHandlers'; + +const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key_for_development'; + +export interface AuthenticatedRequest extends Request { + user?: { + id: string; + username: string; + type?: string; + }; +} + +export const jwtMiddleware = () => { + return (req: Request, res: Response, next: NextFunction) => { + // Log untuk debugging + console.log('Request Path:', req.path); + console.log('Authorization Header:', req.headers.authorization); + console.log('Query Token:', req.query.token); + + // Daftar rute publik + const publicRoutes = [ + '/health-check', + '/swagger', + '/swagger.json', + '/auth/login', + '/images' + ]; + + // Rute download khusus + const downloadRoutes = [ + '/powerpoint-generator/downloads', + '/powerpoint-generator/download' + ]; + + // Lewati middleware untuk rute publik + if (publicRoutes.some(route => req.path.includes(route))) { + return next(); + } + + // Ambil token dari header atau query parameter atau path untuk file statis + let token: string | undefined; + + // Cek header Authorization + if (req.headers.authorization) { + const tokenParts = req.headers.authorization.split(' '); + if (tokenParts.length === 2 && tokenParts[0] === 'Bearer') { + token = tokenParts[1]; + } + } + + // Jika tidak ada token di header, cek query parameter + if (!token && req.query.token) { + token = req.query.token as string; + } + + // Untuk file statis, cek token di path + if (!token && downloadRoutes.some(route => req.path.includes(route))) { + // Ekstrak token dari nama file + const filename = path.basename(req.path); + const tokenMatch = filename.match(/\[([^\]]+)\]/); + if (tokenMatch) { + token = tokenMatch[1]; + } + } + + // Cek keberadaan token + if (!token) { + console.log('No Authorization Token'); + const serviceResponse = new ServiceResponse( + ResponseStatus.Failed, + 'No token provided', + null, + StatusCodes.UNAUTHORIZED + ); + return handleServiceResponse(serviceResponse, res); + } + + try { + // Verifikasi token + console.log('Verifying Token:', token.substring(0, 10) + '...'); + + const decoded = jwt.verify(token, JWT_SECRET) as { + id: string; + username: string; + type?: string; + exp: number + }; + + console.log('Token Decoded:', { + id: decoded.id, + username: decoded.username, + type: decoded.type + }); + + // Tambahkan informasi pengguna ke request + (req as AuthenticatedRequest).user = { + id: decoded.id, + username: decoded.username, + type: decoded.type + }; + + next(); + } catch (error) { + console.error('Token Verification Error:', error); + + let message = 'Invalid token'; + + if (error instanceof jwt.TokenExpiredError) { + message = 'Token expired'; + } else if (error instanceof jwt.JsonWebTokenError) { + message = 'Invalid token signature'; + } + + const serviceResponse = new ServiceResponse( + ResponseStatus.Failed, + message, + null, + StatusCodes.UNAUTHORIZED + ); + return handleServiceResponse(serviceResponse, res); + } + }; +}; diff --git a/src/common/middleware/rateLimiter.ts b/src/common/middleware/rateLimiter.ts index 849b53a..5585c28 100644 --- a/src/common/middleware/rateLimiter.ts +++ b/src/common/middleware/rateLimiter.ts @@ -6,7 +6,7 @@ import { logger } from '@/server'; const rateLimiter = rateLimit({ legacyHeaders: true, - limit: env.COMMON_RATE_LIMIT_MAX_REQUESTS ?? 20, + limit: env.COMMON_RATE_LIMIT_MAX_REQUESTS ?? 100, message: 'Too many requests, please try again later.', standardHeaders: true, windowMs: 15 * 60 * (env.COMMON_RATE_LIMIT_WINDOW_MS ?? 1000), diff --git a/src/index.ts b/src/index.ts old mode 100755 new mode 100644 diff --git a/src/routes/excelGenerator/excelGeneratorModel.ts b/src/routes/excelGenerator/excelGeneratorModel.ts index 3429c25..642a1e9 100644 --- a/src/routes/excelGenerator/excelGeneratorModel.ts +++ b/src/routes/excelGenerator/excelGeneratorModel.ts @@ -12,88 +12,6 @@ export const ExcelGeneratorResponseSchema = z.object({ }); // Request Body Schema -export const ExcelGeneratorRequestBodySchema = z - .object({ - sheetsData: z - .array( - z.object({ - sheetName: z.string().openapi({ - description: 'The name of the sheet to be created in the Excel file.', - }), - tables: z - .array( - z.object({ - title: z.string().optional().openapi({ - description: 'The title of the table, which will be displayed in the first row.', - }), - startCell: z.string().optional().default('A1').openapi({ - description: - "The starting cell (e.g., 'A1') where the table will begin. Defaults to A1 if not specified.", - }), - columns: z - .array( - z.object({ - name: z.string().openapi({ - description: 'The name of the column.', - }), - type: z.enum(['string', 'number', 'boolean', 'percent', 'currency', 'date']).openapi({ - description: 'The data type of the column.', - }), - format: z.string().optional().openapi({ - description: "The format of the column (e.g., '0.00%', '$#,##0', etc.).", - }), - }) - ) - .openapi({ - description: 'The list of columns in the table, each with a name, type, and optional format.', - }), - rows: z - .array( - z.array( - z.object({ - type: z.enum(['static_value', 'formula']).openapi({ - description: 'Indicates whether the cell contains a static value or a formula.', - }), - value: z.string().openapi({ - description: 'The actual value or formula for the cell.', - }), - }) - ) - ) - .openapi({ - description: 'Array of rows in the table, where each row is an array of cells.', - }), - skipHeader: z.boolean().optional().openapi({ - description: 'Whether to skip the header row for this table.', - }), - }) - ) - .openapi({ - description: 'The tables to include in the sheet.', - }), - }) - ) - .openapi({ - description: 'An array of sheet data, where each sheet contains a name and an array of tables to generate.', - }), - excelConfigs: z - .object({ - fontFamily: z.string().optional(), - fontSize: z.number().optional(), - headerFontSize: z.number().optional(), - tableTitleFontSize: z.number().optional(), - borderStyle: z.enum(['none', 'thin', 'double', 'dashed', 'thick']).optional(), - autoFitColumnWidth: z.boolean().optional(), - autoFilter: z.boolean().optional(), - wrapText: z.boolean().optional(), - }) - .optional() - .openapi({ - description: 'Configuration for the Excel file.', - }), - }) - .openapi({ - description: 'Request body for generating an Excel file.', - }); +export const ExcelGeneratorRequestBodySchema = z.object({}); export type ExcelGeneratorRequestBody = z.infer; diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index cc7570c..6fab892 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -1,33 +1,72 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; import * as ExcelJS from 'exceljs'; -import express, { Request, Response, Router } from 'express'; +import express, { NextFunction, Request, Response, Router } from 'express'; import fs from 'fs'; import { StatusCodes } from 'http-status-codes'; import cron from 'node-cron'; import path from 'path'; - +import { z } from 'zod'; import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; import { handleServiceResponse } from '@/common/utils/httpHandlers'; import { ExcelGeneratorRequestBodySchema, ExcelGeneratorResponseSchema } from './excelGeneratorModel'; +import { jwtMiddleware } from '@/common/middleware/jwtMiddleware'; export const COMPRESS = true; export const excelGeneratorRegistry = new OpenAPIRegistry(); excelGeneratorRegistry.register('ExcelGenerator', ExcelGeneratorResponseSchema); + + +// Registrasi path OpenAPI excelGeneratorRegistry.registerPath({ method: 'post', path: '/excel-generator/generate', tags: ['Excel Generator'], + security: [{ bearerAuth: [] }], // Tambahkan autentikasi request: { body: createApiRequestBody(ExcelGeneratorRequestBodySchema, 'application/json'), }, - responses: createApiResponse(ExcelGeneratorResponseSchema, 'Success'), + responses: { + ...createApiResponse(ExcelGeneratorResponseSchema, 'Success'), + 400: { + description: 'Bad Request', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, + 401: { + description: 'Unauthorized', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, + 500: { + description: 'Internal Server Error', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, + }, }); - // Create folder to contains generated files const exportsDir = path.join(__dirname, '../../..', 'excel-exports'); - +const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key'; // Ensure the exports directory exists if (!fs.existsSync(exportsDir)) { fs.mkdirSync(exportsDir, { recursive: true }); @@ -67,33 +106,46 @@ cron.schedule('0 * * * *', () => { }); }); -const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; +const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:8080'; + +// Ensure type safety for sheet data and excel configs +interface CellData { + type: 'static_value' | 'formula'; + value: string | number; +} + +interface ColumnConfig { + name: string; + type: 'string' | 'number' | 'boolean' | 'date' | 'percent' | 'currency'; + format?: string; +} + +interface TableConfig { + title?: string; + startCell: string; + rows: CellData[][]; + columns: ColumnConfig[]; + skipHeader?: boolean; +} interface SheetData { sheetName: string; - tables: { - title: string; - startCell: string; - rows: { - type: string; // static_value or formula, - value: string; - }[][]; - columns: { name: string; type: string; format: string }[]; // types that have format, number, percent, currency - skipHeader: boolean; - }[]; + tables: TableConfig[]; } interface ExcelConfig { - fontFamily: string; - tableTitleFontSize: number; - headerFontSize: number; - fontSize: number; - autoFitColumnWidth: boolean; - autoFilter: boolean; - borderStyle: ExcelJS.BorderStyle | null; // thin, double, dashed, thick - wrapText: boolean; + fontFamily?: string; + tableTitleFontSize?: number; + headerFontSize?: number; + fontSize?: number; + autoFitColumnWidth?: boolean; + autoFilter?: boolean; + borderStyle?: ExcelJS.BorderStyle | null; + wrapText?: boolean; } + +// Default configuration with full type annotations const DEFAULT_EXCEL_CONFIGS: ExcelConfig = { fontFamily: 'Calibri', tableTitleFontSize: 13, @@ -146,7 +198,16 @@ function autoFitColumns( } } -export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelConfig): string { +export function execGenExcelFuncs( + sheetsData: SheetData[], + excelConfigs: ExcelConfig = DEFAULT_EXCEL_CONFIGS +): Promise { + // Merge provided configs with defaults, ensuring complete coverage + const mergedConfigs: ExcelConfig = { + ...DEFAULT_EXCEL_CONFIGS, + ...excelConfigs + }; + const workbook = new ExcelJS.Workbook(); const borderConfigs = excelConfigs.borderStyle ? { @@ -186,7 +247,7 @@ export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelCo sheetsData.forEach(({ sheetName, tables }) => { const worksheet = workbook.addWorksheet(sheetName); - tables.forEach(({ startCell = 'A1', title, rows = [], columns = [], skipHeader }) => { + tables.forEach(({ startCell, title, rows = [], columns = [], skipHeader }) => { const startCol = columnLetterToNumber(startCell[0]); // Convert column letter to index (e.g., 'A' -> 1) const startRow = parseInt(startCell.slice(1)); // Extract the row number (e.g., 'A1' -> 1) let rowIndex = startRow; // Set the initial row index to startRow for each table @@ -318,58 +379,108 @@ export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelCo }); // Write the workbook to a file - const fileName = `excel-file-${new Date().toISOString().replace(/\D/gi, '')}.xlsx`; + const fileName = `maia-excel-${new Date().toISOString().replace(/\D/gi, '')}.xlsx`; const filePath = path.join(exportsDir, fileName); - workbook.xlsx - .writeFile(filePath) - .then(() => { - console.log('File has been written to', filePath); - }) - .catch((err) => { - console.error('Error writing Excel file', err); - }); - return fileName; + return new Promise((resolve, reject) => { + workbook.xlsx + .writeFile(filePath) + .then(() => { + console.log('File has been written to', filePath); + resolve(fileName); + }) + .catch((err) => { + console.error('Error writing Excel file', err); + reject(err); + }); + }); } export const excelGeneratorRouter: Router = (() => { const router = express.Router(); - // Static route for downloading files - router.use('/downloads', express.static(exportsDir)); + // Middleware kustom untuk file statis dengan token + const tokenizedStaticMiddleware = (req: Request, res: Response, next: NextFunction) => { + // Tambahkan token ke query jika ada di path + const token = req.query.token; + if (token) { + return jwtMiddleware()(req, res, next); + } + next(); + }; + + // Gunakan middleware JWT untuk route downloads + router.use('/downloads', + tokenizedStaticMiddleware, + express.static(exportsDir, { + setHeaders: (res, filePath) => { + const isValidFile = filePath.startsWith(exportsDir); + if (!isValidFile) { + res.status(403).json({ + message: 'Access denied' + }); + } + }, + fallthrough: false + }) + ); - router.post('/generate', async (_req: Request, res: Response) => { - const { sheetsData, excelConfigs } = _req.body; // TODO: extract excel config object from request body - if (!sheetsData.length) { - const validateServiceResponse = new ServiceResponse( - ResponseStatus.Failed, - '[Validation Error] Sheets data is required!', - 'Please make sure you have sent the excel sheets content generated from TypingMind.', - StatusCodes.BAD_REQUEST + router.post('/generate', async (req: Request, res: Response) => { + const { sheetsData, excelConfigs } = req.body; // TODO: extract excel config object from request body + // Add explicit null/undefined check + if (!sheetsData || !Array.isArray(sheetsData) || sheetsData.length === 0) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Sheets data is required!', + 'Please make sure you have sent the excel sheets content generated from TypingMind.', + StatusCodes.BAD_REQUEST + ); + return handleServiceResponse(validateServiceResponse, res); + } + + try { + // console.log('Received request body:', JSON.stringify(req.body, null, 2)); + const authHeader = req.headers.authorization; + const token = authHeader ? authHeader.split(' ')[1] : null; + const { + sheetsData, + excelConfigs = {} // Provide a default empty object + } = req.body || {}; + // Comprehensive input validation + if (!sheetsData || !Array.isArray(sheetsData) || sheetsData.length === 0) { + return handleServiceResponse( + new ServiceResponse( + ResponseStatus.Failed, + 'Validation Error', + 'Sheets data is required and must be a non-empty array', + StatusCodes.BAD_REQUEST + ), + res ); - return handleServiceResponse(validateServiceResponse, res); } - try { - const fileName = execGenExcelFuncs(sheetsData, { - fontFamily: excelConfigs.fontFamily ?? DEFAULT_EXCEL_CONFIGS.fontFamily, - tableTitleFontSize: excelConfigs.titleFontSize ?? DEFAULT_EXCEL_CONFIGS.tableTitleFontSize, - headerFontSize: excelConfigs.headerFontSize ?? DEFAULT_EXCEL_CONFIGS.headerFontSize, - fontSize: excelConfigs.fontSize ?? DEFAULT_EXCEL_CONFIGS.fontSize, - autoFilter: excelConfigs.autoFilter ?? DEFAULT_EXCEL_CONFIGS.autoFilter, - borderStyle: - excelConfigs.borderStyle || excelConfigs.borderStyle !== 'none' - ? excelConfigs.borderStyle - : DEFAULT_EXCEL_CONFIGS.borderStyle, - wrapText: excelConfigs.wrapText ?? DEFAULT_EXCEL_CONFIGS.wrapText, - autoFitColumnWidth: excelConfigs.autoFitColumnWidth ?? DEFAULT_EXCEL_CONFIGS.autoFitColumnWidth, - }); + // Create a completely safe configuration object with fallbacks + const validatedExcelConfigs: ExcelConfig = { + fontFamily: excelConfigs?.fontFamily ?? DEFAULT_EXCEL_CONFIGS.fontFamily, + tableTitleFontSize: excelConfigs?.titleFontSize ?? DEFAULT_EXCEL_CONFIGS.tableTitleFontSize, + headerFontSize: excelConfigs?.headerFontSize ?? DEFAULT_EXCEL_CONFIGS.headerFontSize, + fontSize: excelConfigs?.fontSize ?? DEFAULT_EXCEL_CONFIGS.fontSize, + autoFilter: excelConfigs?.autoFilter ?? DEFAULT_EXCEL_CONFIGS.autoFilter, + borderStyle: + excelConfigs?.borderStyle && excelConfigs.borderStyle !== 'none' + ? excelConfigs.borderStyle + : DEFAULT_EXCEL_CONFIGS.borderStyle, + wrapText: excelConfigs?.wrapText ?? DEFAULT_EXCEL_CONFIGS.wrapText, + autoFitColumnWidth: excelConfigs?.autoFitColumnWidth ?? DEFAULT_EXCEL_CONFIGS.autoFitColumnWidth, + }; + // Generate Excel file + const fileName = await execGenExcelFuncs(sheetsData, validatedExcelConfigs); const serviceResponse = new ServiceResponse( ResponseStatus.Success, 'File generated successfully', { - downloadUrl: `${serverUrl}/excel-generator/downloads/${fileName}`, + downloadUrl: `${serverUrl}/excel-generator/downloads/${fileName}?token=${token}`, }, StatusCodes.OK ); diff --git a/src/routes/healthCheck/healthCheckRouter.ts b/src/routes/healthCheck/healthCheckRouter.ts index 64c8033..bea00ba 100644 --- a/src/routes/healthCheck/healthCheckRouter.ts +++ b/src/routes/healthCheck/healthCheckRouter.ts @@ -9,18 +9,45 @@ import { handleServiceResponse } from '@/common/utils/httpHandlers'; export const healthCheckRegistry = new OpenAPIRegistry(); +// Skema respons yang lebih detail +const HealthCheckResponseSchema = z.object({ + status: z.string(), + timestamp: z.string(), + uptime: z.number(), +}); + export const healthCheckRouter: Router = (() => { const router = express.Router(); healthCheckRegistry.registerPath({ method: 'get', path: '/health-check', - tags: ['Health Check'], - responses: createApiResponse(z.null(), 'Success'), + description: 'Health check endpoint', + security: [], // Tidak memerlukan autentikasi + responses: { + 200: { + description: 'Successful health check', + content: { + 'application/json': { + schema: HealthCheckResponseSchema, + }, + }, + }, + }, }); router.get('/', (_req: Request, res: Response) => { - const serviceResponse = new ServiceResponse(ResponseStatus.Success, 'Service is healthy', null, StatusCodes.OK); + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'Service is healthy', + { + status: 'OK', + timestamp: new Date().toISOString(), + uptime: process.uptime(), // Waktu uptime server + }, + StatusCodes.OK + ); + handleServiceResponse(serviceResponse, res); }); diff --git a/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts b/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts index 8d0f543..ebf3e8a 100644 --- a/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts +++ b/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts @@ -1,34 +1,87 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import express, { Request, Response, Router } from 'express'; +import express, { NextFunction, Request, Response, Router } from 'express'; import fs from 'fs'; import { StatusCodes } from 'http-status-codes'; import cron from 'node-cron'; import path from 'path'; import pptxgen from 'pptxgenjs'; +import { z } from 'zod'; import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; import { handleServiceResponse } from '@/common/utils/httpHandlers'; +import jwt from 'jsonwebtoken'; +import { + PowerpointGeneratorRequestBodySchema, + PowerpointGeneratorResponseSchema +} from './powerpointGeneratorModel'; +import { AuthenticatedRequest, jwtMiddleware } from '@/common/middleware/jwtMiddleware'; -import { PowerpointGeneratorRequestBodySchema, PowerpointGeneratorResponseSchema } from './powerpointGeneratorModel'; export const COMPRESS = true; // API Doc definition export const powerpointGeneratorRegistry = new OpenAPIRegistry(); powerpointGeneratorRegistry.register('PowerpointGenerator', PowerpointGeneratorResponseSchema); + +// Validasi skema request yang lebih ketat +const ValidatedPowerpointGeneratorRequestBodySchema = PowerpointGeneratorRequestBodySchema.refine( + (data) => { + // Tambahkan validasi kustom + return data.slides && data.slides.length > 0; + }, + { message: 'Slides must be a non-empty array' } +); + powerpointGeneratorRegistry.registerPath({ method: 'post', path: '/powerpoint-generator/generate', tags: ['Powerpoint Generator'], + security: [{ bearerAuth: [] }], // Tambahkan autentikasi request: { - body: createApiRequestBody(PowerpointGeneratorRequestBodySchema, 'application/json'), + body: createApiRequestBody(ValidatedPowerpointGeneratorRequestBodySchema, 'application/json'), + }, + responses: { + ...createApiResponse(PowerpointGeneratorResponseSchema, 'Success'), + 400: { + description: 'Bad Request', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, + 401: { + description: 'Unauthorized', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, + 500: { + description: 'Internal Server Error', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, }, - responses: createApiResponse(PowerpointGeneratorResponseSchema, 'Success'), }); // Create folder to contains generated files const exportsDir = path.join(__dirname, '../../..', 'powerpoint-exports'); +const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key'; // Ensure the exports directory exists if (!fs.existsSync(exportsDir)) { fs.mkdirSync(exportsDir, { recursive: true }); @@ -68,7 +121,7 @@ cron.schedule('0 * * * *', () => { }); }); -const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; +const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:8080'; // Define configurable options for layout, font size, and font family const defaultSlideConfig = { @@ -445,7 +498,7 @@ async function execGenSlidesFuncs(slides: any[], config: any) { } }); - const fileName = `your-presentation-${new Date().toISOString().replace(/\D/gi, '')}`; + const fileName = `maia-presentation-${new Date().toISOString().replace(/\D/gi, '')}`; const filePath = path.join(exportsDir, fileName); await pptx.writeFile({ @@ -458,22 +511,54 @@ async function execGenSlidesFuncs(slides: any[], config: any) { export const powerpointGeneratorRouter: Router = (() => { const router = express.Router(); - // Static route for downloading files - router.use('/downloads', express.static(exportsDir)); - - router.post('/generate', async (_req: Request, res: Response) => { - const { slides = [], slideConfig = {} } = _req.body; - if (!slides.length) { + // Middleware kustom untuk file statis dengan token + const tokenizedStaticMiddleware = (req: Request, res: Response, next: NextFunction) => { + // Tambahkan token ke query jika ada di path + const token = req.query.token; + if (token) { + return jwtMiddleware()(req, res, next); + } + next(); + }; + + // Gunakan middleware JWT untuk route downloads + router.use('/downloads', + tokenizedStaticMiddleware, + express.static(exportsDir, { + setHeaders: (res, filePath) => { + const isValidFile = filePath.startsWith(exportsDir); + if (!isValidFile) { + res.status(403).json({ + message: 'Access denied' + }); + } + }, + fallthrough: false + }) + ); + + +router.post('/generate', async (req: Request, res: Response) => { + const { slides = [], slideConfig = {} } = req.body; + // Validasi input yang lebih komprehensif + if (!slides || !Array.isArray(slides) || slides.length === 0) { const validateServiceResponse = new ServiceResponse( ResponseStatus.Failed, - '[Validation Error] Presentation slides is required!', - 'Please make sure you have sent the slide content generated from TypingMind.', + 'Validation Error: Presentation slides are required', + 'Please ensure you have sent valid slide content', StatusCodes.BAD_REQUEST ); return handleServiceResponse(validateServiceResponse, res); } try { + // Log permintaan untuk debugging + console.log(`[Powerpoint Generator] Generating presentation with ${slides.length} slides`); + // Dapatkan token dari header Authorization + // Dapatkan token dari header Authorization + const authHeader = req.headers.authorization; + const token = authHeader ? authHeader.split(' ')[1] : null; + const fileName = await execGenSlidesFuncs(slides, { layout: slideConfig.layout === '' ? defaultSlideConfig.layout : slideConfig.layout, // Default: LAYOUT_WIDE, enum: LAYOUT_16x9 10 x 5.625 inches, LAYOUT_16x10 10 x 6.25 inches, LAYOUT_4x3 10 x 7.5 inches titleFontSize: slideConfig.titleFontSize === 0 ? defaultSlideConfig.titleFontSize : slideConfig.titleFontSize, // Default: 52, Emphasize the main topic in Title Slide @@ -514,16 +599,21 @@ export const powerpointGeneratorRouter: Router = (() => { tableTextColor: slideConfig.tableTextColor === '' ? defaultSlideConfig.tableTextColor : slideConfig.tableTextColor, // Default: '#000000', Text color inside the table }); - const serviceResponse = new ServiceResponse( - ResponseStatus.Success, - 'File generated successfully', - { - downloadUrl: `${serverUrl}/powerpoint-generator/downloads/${fileName}`, - }, - StatusCodes.OK - ); - return handleServiceResponse(serviceResponse, res); + + // Tambahkan token sebagai query parameter jika tersedia + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'File generated successfully', + { + // Tambahkan token ke URL download + downloadUrl: `${serverUrl}/powerpoint-generator/downloads/${fileName}?token=${token}`, + }, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); } catch (error) { + // Log error untuk debugging + console.error(`[Powerpoint Generator] Error:`, error); const errorMessage = (error as Error).message; let responseObject = ''; if (errorMessage.includes('')) { diff --git a/src/routes/webPageReader/webPageReaderRouter.ts b/src/routes/webPageReader/webPageReaderRouter.ts index ee4adf0..8ac722f 100644 --- a/src/routes/webPageReader/webPageReaderRouter.ts +++ b/src/routes/webPageReader/webPageReaderRouter.ts @@ -11,6 +11,7 @@ import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse import { handleServiceResponse } from '@/common/utils/httpHandlers'; import { WebPageReaderRequestParamSchema, WebPageReaderResponseSchema } from './webPageReaderModel'; +import { z } from 'zod'; export const articleReaderRegistry = new OpenAPIRegistry(); articleReaderRegistry.register('Web Page Reader', WebPageReaderResponseSchema); @@ -66,12 +67,38 @@ export const webPageReaderRouter: Router = (() => { method: 'get', path: '/web-page-reader/get-content', tags: ['Web Page Reader'], + security: [{ bearerAuth: [] }], // Tambahkan autentikasi request: { query: WebPageReaderRequestParamSchema, }, - responses: createApiResponse(WebPageReaderResponseSchema, 'Success'), + responses: { + ...createApiResponse(WebPageReaderResponseSchema, 'Success'), + 401: { + description: 'Unauthorized', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, + 400: { + description: 'Bad Request', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, + }, }); + router.get('/get-content', async (_req: Request, res: Response) => { const { url } = _req.query; diff --git a/src/routes/wordGenerator/wordGeneratorRouter.ts b/src/routes/wordGenerator/wordGeneratorRouter.ts index 8823b0a..5d69e92 100644 --- a/src/routes/wordGenerator/wordGeneratorRouter.ts +++ b/src/routes/wordGenerator/wordGeneratorRouter.ts @@ -19,33 +19,80 @@ import { TextRun, WidthType, } from 'docx'; -import express, { Request, Response, Router } from 'express'; +import express, { NextFunction, Request, Response, Router } from 'express'; import fs from 'fs'; import { StatusCodes } from 'http-status-codes'; import cron from 'node-cron'; import path from 'path'; - +import { z } from 'zod'; import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; import { handleServiceResponse } from '@/common/utils/httpHandlers'; import { WordGeneratorRequestBodySchema, WordGeneratorResponseSchema } from './wordGeneratorModel'; +import { jwtMiddleware } from '@/common/middleware/jwtMiddleware'; export const COMPRESS = true; export const wordGeneratorRegistry = new OpenAPIRegistry(); wordGeneratorRegistry.register('WordGenerator', WordGeneratorResponseSchema); +// Validasi skema request yang lebih ketat +const ValidatedWordGeneratorRequestBodySchema = WordGeneratorRequestBodySchema.refine( + (data) => { + // Tambahkan validasi kustom + return data.sections && data.sections.length > 0 && data.title && data.title.trim() !== ''; + }, + { message: 'Sections must be a non-empty array and title must be provided' } +); + wordGeneratorRegistry.registerPath({ method: 'post', path: '/word-generator/generate', tags: ['Word Generator'], + security: [{ bearerAuth: [] }], // Tambahkan autentikasi request: { - body: createApiRequestBody(WordGeneratorRequestBodySchema, 'application/json'), + body: createApiRequestBody(ValidatedWordGeneratorRequestBodySchema, 'application/json'), + }, + responses: { + ...createApiResponse(WordGeneratorResponseSchema, 'Success'), + 400: { + description: 'Bad Request', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, + 401: { + description: 'Unauthorized', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, + 500: { + description: 'Internal Server Error', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, }, - responses: createApiResponse(WordGeneratorResponseSchema, 'Success'), }); // Create folder to contains generated files const exportsDir = path.join(__dirname, '../../..', 'word-exports'); +const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key'; // Ensure the exports directory exists if (!fs.existsSync(exportsDir)) { fs.mkdirSync(exportsDir, { recursive: true }); @@ -85,7 +132,7 @@ cron.schedule('0 * * * *', () => { }); }); -const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; +const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:8080'; const FONT_CONFIG = { size: 12, // Font size in point @@ -575,7 +622,7 @@ async function execGenWordFuncs( footnotes: footnoteConfig, // TODO: Enhance footnote }); - const fileName = `word-file-${new Date().toISOString().replace(/\D/gi, '')}.docx`; + const fileName = `word-maia-${new Date().toISOString().replace(/\D/gi, '')}.docx`; const filePath = path.join(exportsDir, fileName); // Create and save the document @@ -588,11 +635,34 @@ async function execGenWordFuncs( export const wordGeneratorRouter: Router = (() => { const router = express.Router(); + // Middleware kustom untuk file statis dengan token + const tokenizedStaticMiddleware = (req: Request, res: Response, next: NextFunction) => { + // Tambahkan token ke query jika ada di path + const token = req.query.token; + if (token) { + return jwtMiddleware()(req, res, next); + } + next(); + }; // Static route for downloading files - router.use('/downloads', express.static(exportsDir)); + // Gunakan middleware JWT untuk route downloads + router.use('/downloads', + tokenizedStaticMiddleware, + express.static(exportsDir, { + setHeaders: (res, filePath) => { + const isValidFile = filePath.startsWith(exportsDir); + if (!isValidFile) { + res.status(403).json({ + message: 'Access denied' + }); + } + }, + fallthrough: false + }) + ); - router.post('/generate', async (_req: Request, res: Response) => { - const { title, sections = [], header, footer, wordConfig = {} } = _req.body; + router.post('/generate', async (req: Request, res: Response) => { + const { title, sections = [], header, footer, wordConfig = {} } = req.body; if (!sections.length) { const validateServiceResponse = new ServiceResponse( ResponseStatus.Failed, @@ -604,6 +674,9 @@ export const wordGeneratorRouter: Router = (() => { } try { + console.log(`[Word Generator] Generating Word with ${sections.length}`); + const authHeader = req.headers.authorization; + const token = authHeader ? authHeader.split(' ')[1] : null; const wordConfigs = { numberingReference: wordConfig.showNumberingInHeader ? wordConfig.numberingReference : '', showPageNumber: wordConfig.showPageNumber ?? false, @@ -628,7 +701,7 @@ export const wordGeneratorRouter: Router = (() => { ResponseStatus.Success, 'File generated successfully', { - downloadUrl: `${serverUrl}/word-generator/downloads/${fileName}`, + downloadUrl: `${serverUrl}/word-generator/downloads/${fileName}?token=${token}`, }, StatusCodes.OK ); diff --git a/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts b/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts index 9ee3fd0..ca6c46d 100644 --- a/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts +++ b/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts @@ -8,6 +8,7 @@ import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse import { handleServiceResponse } from '@/common/utils/httpHandlers'; import { YoutubeTranscriptRequestParamSchema, YoutubeTranscriptResponseSchema } from './youtubeTranscriptModel'; +import { z } from 'zod'; export const youtubeTranscriptRegistry = new OpenAPIRegistry(); youtubeTranscriptRegistry.register('YoutubeTranscript', YoutubeTranscriptResponseSchema); @@ -19,15 +20,40 @@ export const youtubeTranscriptRouter: Router = (() => { method: 'get', path: '/youtube-transcript/get-transcript', tags: ['Youtube Transcript'], + security: [{ bearerAuth: [] }], // Tambahkan autentikasi request: { query: YoutubeTranscriptRequestParamSchema, }, - responses: createApiResponse(YoutubeTranscriptResponseSchema, 'Success'), + responses: { + ...createApiResponse(YoutubeTranscriptResponseSchema, 'Success'), + 401: { + description: 'Unauthorized', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, + 400: { + description: 'Bad Request', + content: { + 'application/json': { + schema: z.object({ + message: z.string(), + error: z.string().optional(), + }), + }, + }, + }, + }, }); router.get('/get-transcript', async (_req: Request, res: Response) => { console.log('Head to get-transcript'); - const { query : videoId } = _req.query; + const { videoId } = _req.query; console.log('Head to get-transcript -> ', videoId); if (!videoId) { diff --git a/src/server.ts b/src/server.ts index 90d8872..9345bef 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,48 +3,71 @@ import cors from 'cors'; import express, { Express } from 'express'; import helmet from 'helmet'; import { pino } from 'pino'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); import { openAPIRouter } from '@/api-docs/openAPIRouter'; import errorHandler from '@/common/middleware/errorHandler'; import rateLimiter from '@/common/middleware/rateLimiter'; import requestLogger from '@/common/middleware/requestLogger'; +import { jwtMiddleware } from '@/common/middleware/jwtMiddleware'; import { healthCheckRouter } from '@/routes/healthCheck/healthCheckRouter'; +// Import routers import { excelGeneratorRouter } from './routes/excelGenerator/excelGeneratorRouter'; import { notionDatabaseRouter } from './routes/notionDatabase/notionDatabaseRouter'; import { powerpointGeneratorRouter } from './routes/powerpointGenerator/powerpointGeneratorRouter'; import { webPageReaderRouter } from './routes/webPageReader/webPageReaderRouter'; import { wordGeneratorRouter } from './routes/wordGenerator/wordGeneratorRouter'; import { youtubeTranscriptRouter } from './routes/youtubeTranscript/youtubeTranscriptRouter'; +import authRouter from './auth/authRouter'; + const logger = pino({ name: 'server start' }); const app: Express = express(); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +// Pastikan secret key ada +if (!process.env.JWT_SECRET) { + console.error('JWT_SECRET is not defined in environment variables'); + process.exit(1); +} -// Set the application to trust the reverse proxy -app.set('trust proxy', true); // Middlewares -// app.use(cors({ origin: env.CORS_ORIGIN, credentials: true })); app.use(cors()); app.use(helmet()); app.use(rateLimiter); app.use(bodyParser.json()); -app.use((req, res, next) => { - res.removeHeader('X-Frame-Options'); - res.removeHeader('Content-Security-Policy'); - next(); -}); -// Request logging app.use(requestLogger()); -// Routes +// Public Routes app.use('/health-check', healthCheckRouter); +app.use('/auth', authRouter); // Tambahkan route autentikasi app.use('/images', express.static('public/images')); + + + +app.use([ + '/youtube-transcript', + '/web-page-reader', + '/powerpoint-generator/generate', + '/word-generator', + '/excel-generator', + '/notion-database' +], jwtMiddleware()); + + +// Protected Routes app.use('/youtube-transcript', youtubeTranscriptRouter); app.use('/web-page-reader', webPageReaderRouter); app.use('/powerpoint-generator', powerpointGeneratorRouter); app.use('/word-generator', wordGeneratorRouter); app.use('/excel-generator', excelGeneratorRouter); app.use('/notion-database', notionDatabaseRouter); - +app.get('/', (req, res) => { + res.status(200).json({ status: 'ok' }); +}); // Swagger UI app.use(openAPIRouter); diff --git a/tsconfig.json b/tsconfig.json index 73b6047..5450097 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,10 +14,14 @@ "strict": true, "noPropertyAccessFromIndexSignature": false, "skipLibCheck": true, - "ignoreDeprecations": "5.0", - "types": ["vitest/globals"] + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "types": ["vitest/globals", "node"] }, - "extends": [], - "exclude": ["node_modules"], + "ts-node": { + "esm": true, + "experimentalSpecifierResolution": "node" + }, + "exclude": ["node_modules", "dist"], "include": ["src/**/*.ts"] -} +} \ No newline at end of file From 62bd74681c65f7ae039414c0d00644ba539f0c25 Mon Sep 17 00:00:00 2001 From: Muhamad Dendi Purwanto Date: Tue, 22 Apr 2025 21:03:16 +0700 Subject: [PATCH 2/5] fix-update --- .husky/commit-msg | 4 ---- .husky/pre-commit | 4 ---- .husky/pre-push | 5 ----- 3 files changed, 13 deletions(-) delete mode 100755 .husky/commit-msg delete mode 100755 .husky/pre-commit delete mode 100755 .husky/pre-push diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100755 index 80416c7..0000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx --no-install commitlint --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 0312b76..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx lint-staged \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index e53ab96..0000000 --- a/.husky/pre-push +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npm run test -npm run build \ No newline at end of file From 073efc425d178083f1999ed9991942353c864dba Mon Sep 17 00:00:00 2001 From: Muhamad Dendi Purwanto Date: Tue, 22 Apr 2025 21:08:45 +0700 Subject: [PATCH 3/5] fix-update --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7cf4c63..4759f1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /app COPY package*.json ./ # Install app dependencies -RUN npm Install +RUN npm install # Bundle app source COPY . . From 7d988879689c2c620379232c4854a7fc6aa2fbb6 Mon Sep 17 00:00:00 2001 From: Muhamad Dendi Purwanto Date: Tue, 22 Apr 2025 21:26:35 +0700 Subject: [PATCH 4/5] fix rate limiter --- src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 9345bef..c5f9520 100644 --- a/src/server.ts +++ b/src/server.ts @@ -37,7 +37,7 @@ if (!process.env.JWT_SECRET) { // Middlewares app.use(cors()); app.use(helmet()); -app.use(rateLimiter); +// app.use(rateLimiter); app.use(bodyParser.json()); app.use(requestLogger()); From f79cbb8cbb18691dddcc96a44275e2b5034d078d Mon Sep 17 00:00:00 2001 From: Muhamad Dendi Purwanto Date: Wed, 23 Apr 2025 09:16:25 +0700 Subject: [PATCH 5/5] update plugin server --- package-lock.json | 65 +- package.json | 8 +- src/api-docs/openAPIDocumentGenerator.ts | 20 - src/auth/auth.controller.ts | 34 ++ src/auth/auth.module.ts | 2 +- src/auth/authRouter.ts | 100 ++-- src/auth/jwt.guard.ts | 46 +- src/auth/jwt.strategy.ts | 23 +- src/common/middleware/jwtMiddleware.ts | 138 +---- src/common/middleware/rateLimiter.ts | 2 +- src/index.ts | 1 - .../excelGenerator/excelGeneratorRouter.ts | 231 ++------ src/routes/healthCheck/healthCheckRouter.ts | 33 +- .../powerpointGeneratorRouter.ts | 555 +++++++----------- .../webPageReader/webPageReaderRouter.ts | 29 +- .../wordGenerator/wordGeneratorRouter.ts | 97 +-- .../youtubeTranscriptRouter.ts | 28 +- src/server.ts | 191 ++++-- src/utils/paths.ts | 8 + tsconfig.json | 2 +- 20 files changed, 653 insertions(+), 960 deletions(-) create mode 100644 src/auth/auth.controller.ts create mode 100644 src/utils/paths.ts diff --git a/package-lock.json b/package-lock.json index 964771d..47f068d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,6 @@ "@notionhq/client": "^2.2.15", "@types/got": "^9.6.12", "@types/jsdom": "^21.1.6", - "@types/jsonwebtoken": "^9.0.9", - "@types/passport-jwt": "^4.0.1", "body-parser": "^1.20.2", "cheerio": "^1.0.0-rc.12", "class-transformer": "^0.5.1", @@ -34,7 +32,6 @@ "helmet": "^7.1.0", "http-status-codes": "^2.3.0", "jsdom": "^26.0.0", - "jsonwebtoken": "^9.0.2", "node-cron": "^3.0.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", @@ -53,6 +50,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node-cron": "^3.0.11", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "@typescript-eslint/eslint-plugin": "^7.13.1", @@ -106,7 +104,6 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.3.0.tgz", "integrity": "sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==", - "license": "MIT", "dependencies": { "openapi3-ts": "^4.1.2" }, @@ -1347,7 +1344,6 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", "integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==", - "license": "MIT", "dependencies": { "@types/jsonwebtoken": "9.0.7", "jsonwebtoken": "9.0.2" @@ -1356,20 +1352,10 @@ "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" } }, - "node_modules/@nestjs/jwt/node_modules/@types/jsonwebtoken": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", - "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@nestjs/passport": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", - "license": "MIT", "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" @@ -1967,6 +1953,7 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -1976,6 +1963,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -2014,6 +2002,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -2025,6 +2014,7 @@ "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -2050,7 +2040,8 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true }, "node_modules/@types/jsdom": { "version": "21.1.7", @@ -2063,12 +2054,11 @@ } }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", - "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", "license": "MIT", "dependencies": { - "@types/ms": "*", "@types/node": "*" } }, @@ -2081,13 +2071,8 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true }, "node_modules/@types/node": { "version": "22.10.1", @@ -2135,6 +2120,7 @@ "version": "1.0.17", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, "license": "MIT", "dependencies": { "@types/express": "*" @@ -2144,7 +2130,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", - "license": "MIT", + "dev": true, "dependencies": { "@types/jsonwebtoken": "*", "@types/passport-strategy": "*" @@ -2154,6 +2140,7 @@ "version": "0.2.38", "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, "license": "MIT", "dependencies": { "@types/express": "*", @@ -2163,12 +2150,14 @@ "node_modules/@types/qs": { "version": "6.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==" + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true }, "node_modules/@types/semver": { "version": "7.5.8", @@ -2180,6 +2169,7 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -2189,6 +2179,7 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -2254,8 +2245,7 @@ "node_modules/@types/validator": { "version": "13.12.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", - "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", - "license": "MIT" + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", @@ -3390,14 +3380,12 @@ "node_modules/class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" }, "node_modules/class-validator": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", - "license": "MIT", "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.10.53", @@ -7436,8 +7424,7 @@ "node_modules/libphonenumber-js": { "version": "1.11.20", "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.20.tgz", - "integrity": "sha512-/ipwAMvtSZRdiQBHqW1qxqeYiBMzncOQLVA+62MWYr7N4m7Q2jqpJ0WgT7zlOEOpyLRSqrMXidbJpC0J77AaKA==", - "license": "MIT" + "integrity": "sha512-/ipwAMvtSZRdiQBHqW1qxqeYiBMzncOQLVA+62MWYr7N4m7Q2jqpJ0WgT7zlOEOpyLRSqrMXidbJpC0J77AaKA==" }, "node_modules/lie": { "version": "3.3.0", @@ -8693,7 +8680,6 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", - "license": "MIT", "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -8711,7 +8697,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", - "license": "MIT", "dependencies": { "jsonwebtoken": "^9.0.0", "passport-strategy": "^1.0.0" @@ -11826,7 +11811,6 @@ "version": "13.12.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", - "license": "MIT", "engines": { "node": ">= 0.10" } @@ -13032,7 +13016,6 @@ "version": "3.24.2", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", - "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 5827b0c..5215105 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "description": "TypingMind Proxy", "main": "index.ts", "scripts": { - "deploy": "./deploy.sh", "dev": "tsx watch --clear-screen=false src/index.ts | pino-pretty", "build": "rimraf dist && tsup", "start": "tsx dist/index.js", @@ -26,8 +25,6 @@ "@notionhq/client": "^2.2.15", "@types/got": "^9.6.12", "@types/jsdom": "^21.1.6", - "@types/jsonwebtoken": "^9.0.9", - "@types/passport-jwt": "^4.0.1", "body-parser": "^1.20.2", "cheerio": "^1.0.0-rc.12", "class-transformer": "^0.5.1", @@ -44,7 +41,6 @@ "helmet": "^7.1.0", "http-status-codes": "^2.3.0", "jsdom": "^26.0.0", - "jsonwebtoken": "^9.0.2", "node-cron": "^3.0.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", @@ -63,6 +59,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node-cron": "^3.0.11", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "@typescript-eslint/eslint-plugin": "^7.13.1", @@ -91,5 +88,6 @@ }, "author": "Travis", "repository": "travis-thuanle/typingmind-proxy", - "license": "MIT" + "license": "MIT", + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/api-docs/openAPIDocumentGenerator.ts b/src/api-docs/openAPIDocumentGenerator.ts index 24855dd..b430210 100644 --- a/src/api-docs/openAPIDocumentGenerator.ts +++ b/src/api-docs/openAPIDocumentGenerator.ts @@ -1,6 +1,4 @@ -// src/api-docs/openAPIDocumentGenerator.ts import { OpenApiGeneratorV3, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { z } from 'zod'; import { excelGeneratorRegistry } from '@/routes/excelGenerator/excelGeneratorRouter'; import { healthCheckRegistry } from '@/routes/healthCheck/healthCheckRouter'; @@ -20,19 +18,6 @@ export function generateOpenAPIDocument() { excelGeneratorRegistry, notionDatabaseRegistry, ]); - - // Tambahkan skema keamanan JWT - registry.registerComponent('securitySchemes', 'bearerAuth', { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - }); - - // Contoh skema untuk token (opsional) - const JwtTokenSchema = z.object({ - token: z.string().describe('JWT Authentication Token'), - }); - const generator = new OpenApiGeneratorV3(registry.definitions); return generator.generateDocument({ @@ -45,10 +30,5 @@ export function generateOpenAPIDocument() { description: 'View the raw OpenAPI Specification in JSON format', url: '/swagger.json', }, - security: [ - { - bearerAuth: [], - }, - ], }); } diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..4e423d2 --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,34 @@ +// auth.controller.ts +import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; + +interface LoginDto { + username: string; + password: string; +} + +@Controller('auth') +export class AuthController { + constructor(private jwtService: JwtService) {} + + @Post('login') + async login(@Body() loginDto: LoginDto) { + // For demo purposes - replace with your actual user validation + if (loginDto.username === 'admin' && loginDto.password === 'admin123') { + const payload = { + sub: '1', // user id + email: loginDto.username, + }; + + const token = this.jwtService.sign(payload); + + return { + access_token: token, + token_type: 'Bearer', + expires_in: 3600 // 1 hour + }; + } + + throw new UnauthorizedException('Invalid credentials'); + } +} \ No newline at end of file diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 72208d7..da6e784 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -8,7 +8,7 @@ import { JwtStrategy } from './jwt.strategy'; PassportModule, JwtModule.register({ secret: process.env.JWT_SECRET || 'your-secret-key', - signOptions: { expiresIn: '1h' }, + signOptions: { expiresIn: '5h' }, }), ], providers: [JwtStrategy], diff --git a/src/auth/authRouter.ts b/src/auth/authRouter.ts index 99f43a7..6d7dfde 100644 --- a/src/auth/authRouter.ts +++ b/src/auth/authRouter.ts @@ -1,62 +1,78 @@ +// authRouter.ts import express from 'express'; import jwt from 'jsonwebtoken'; -import { z } from 'zod'; const router = express.Router(); -const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key_for_development'; - -// Skema validasi login untuk kredensial sementara -const LoginSchema = z.object({ - username: z.string().startsWith('tmp_user_', "Username must be a temporary user"), - password: z.string().startsWith('tmp_pass_', "Password must be a temporary password") -}); router.post('/login', (req, res) => { - try { - const { username, password } = req.body; + const { username, password } = req.body; - // Validasi input untuk kredensial sementara - LoginSchema.parse({ username, password }); + // Basic validation to ensure credentials are provided + if (!username || !password) { + return res.status(400).json({ + message: 'Username and password are required' + }); + } - // Generate ID unik untuk pengguna sementara - const userId = `tmp_${Date.now()}_${Math.random().toString(36).substring(7)}`; + // Create a payload using the provided username + const payload = { + sub: Date.now().toString(), // Unique ID based on timestamp + email: username, + // You can add additional claims if needed + loginTime: new Date().toISOString(), + clientId: 'powerpoint-plugin' + }; - // Generate token + try { const token = jwt.sign( + payload, + process.env.JWT_SECRET || 'your-secret-key', { - id: userId, - username: username, - type: 'temporary' - }, - JWT_SECRET, - { - expiresIn: '1h' + expiresIn: '5h', + algorithm: 'HS256' } ); - res.json({ - token, - user: { - id: userId, - username: username - } + // Log successful login attempt (optional) + console.log(`Login successful for user: ${username}`); + + return res.json({ + access_token: token, + token_type: 'Bearer', + expires_in: 18000, + generated_at: new Date().toISOString() + }); + + } catch (error) { + console.error('Token generation error:', error); + return res.status(500).json({ + message: 'Error generating authentication token' + }); + } +}); + +// Optional: Add a token verification endpoint +router.post('/verify', (req, res) => { + const authHeader = req.headers.authorization; + + if (!authHeader) { + return res.status(401).json({ message: 'No token provided' }); + } + + const token = authHeader.split(' ')[1]; + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key'); + return res.json({ + valid: true, + decoded }); } catch (error) { - console.error('Temporary Login Error:', error); - - // Tangani kesalahan validasi atau autentikasi - if (error instanceof z.ZodError) { - return res.status(400).json({ - message: 'Invalid temporary credentials', - errors: error.errors - }); - } - - res.status(401).json({ - message: 'Temporary login failed', - error: error instanceof Error ? error.message : 'Unknown error' + return res.status(401).json({ + valid: false, + message: 'Invalid token' }); } }); -export default router; +export const authRouter = router; \ No newline at end of file diff --git a/src/auth/jwt.guard.ts b/src/auth/jwt.guard.ts index 18588a5..b255b07 100644 --- a/src/auth/jwt.guard.ts +++ b/src/auth/jwt.guard.ts @@ -1,5 +1,41 @@ -import { Injectable } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; - -@Injectable() -export class JwtAuthGuard extends AuthGuard('jwt') {} \ No newline at end of file +import { + Injectable, + ExecutionContext, + UnauthorizedException + } from '@nestjs/common'; + import { AuthGuard } from '@nestjs/passport'; + + @Injectable() + export class JwtAuthGuard extends AuthGuard('jwt') { + canActivate(context: ExecutionContext) { + // Tambahkan logging untuk debugging + console.log('JWT Guard Activated'); + console.log('Context Type:', context.getType()); + + try { + return super.canActivate(context); + } catch (error) { + console.error('JWT Guard Error:', error); + throw new UnauthorizedException('Authentication failed'); + } + } + + handleRequest(err: any, user: any, info: any) { + // Logging untuk memahami proses autentikasi + console.log('Handle Request Error:', err); + console.log('User:', user); + console.log('Info:', info); + + if (err) { + console.error('JWT Verification Error:', err); + throw new UnauthorizedException(err.message || 'Token verification failed'); + } + + if (!user) { + throw new UnauthorizedException('No user found'); + } + + return user; + } + } + \ No newline at end of file diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index 3f5f602..8b807ab 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -1,7 +1,15 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; +// Interface untuk struktur payload +interface JwtPayload { + sub: string; + email: string; + iat?: number; + exp?: number; +} + @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { @@ -12,7 +20,14 @@ export class JwtStrategy extends PassportStrategy(Strategy) { }); } - async validate(payload: any) { - return payload; + async validate(payload: JwtPayload) { + // Validasi payload + if (!payload.sub || !payload.email) { + throw new UnauthorizedException('Invalid token payload'); + } + return { + userId: payload.sub, + email: payload.email + }; } -} \ No newline at end of file +} diff --git a/src/common/middleware/jwtMiddleware.ts b/src/common/middleware/jwtMiddleware.ts index 442e1ae..7e79dbf 100644 --- a/src/common/middleware/jwtMiddleware.ts +++ b/src/common/middleware/jwtMiddleware.ts @@ -1,129 +1,19 @@ import { Request, Response, NextFunction } from 'express'; -import jwt from 'jsonwebtoken'; -import { StatusCodes } from 'http-status-codes'; -import path from 'path'; +import { JwtAuthGuard } from '../../auth/jwt.guard'; -import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; -import { handleServiceResponse } from '@/common/utils/httpHandlers'; +const jwtAuthGuard = new JwtAuthGuard(); -const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key_for_development'; - -export interface AuthenticatedRequest extends Request { - user?: { - id: string; - username: string; - type?: string; - }; -} - -export const jwtMiddleware = () => { - return (req: Request, res: Response, next: NextFunction) => { - // Log untuk debugging - console.log('Request Path:', req.path); - console.log('Authorization Header:', req.headers.authorization); - console.log('Query Token:', req.query.token); - - // Daftar rute publik - const publicRoutes = [ - '/health-check', - '/swagger', - '/swagger.json', - '/auth/login', - '/images' - ]; - - // Rute download khusus - const downloadRoutes = [ - '/powerpoint-generator/downloads', - '/powerpoint-generator/download' - ]; - - // Lewati middleware untuk rute publik - if (publicRoutes.some(route => req.path.includes(route))) { - return next(); - } - - // Ambil token dari header atau query parameter atau path untuk file statis - let token: string | undefined; - - // Cek header Authorization - if (req.headers.authorization) { - const tokenParts = req.headers.authorization.split(' '); - if (tokenParts.length === 2 && tokenParts[0] === 'Bearer') { - token = tokenParts[1]; - } - } - - // Jika tidak ada token di header, cek query parameter - if (!token && req.query.token) { - token = req.query.token as string; - } - - // Untuk file statis, cek token di path - if (!token && downloadRoutes.some(route => req.path.includes(route))) { - // Ekstrak token dari nama file - const filename = path.basename(req.path); - const tokenMatch = filename.match(/\[([^\]]+)\]/); - if (tokenMatch) { - token = tokenMatch[1]; +export const jwtMiddleware = (req: Request, res: Response, next: NextFunction) => { + jwtAuthGuard.canActivate(req).then( + (result: any) => { + if (result) { + next(); + } else { + res.status(401).json({ message: 'Unauthorized' }); } + }, + (error) => { + res.status(401).json({ message: 'Unauthorized', error: error.message }); } - - // Cek keberadaan token - if (!token) { - console.log('No Authorization Token'); - const serviceResponse = new ServiceResponse( - ResponseStatus.Failed, - 'No token provided', - null, - StatusCodes.UNAUTHORIZED - ); - return handleServiceResponse(serviceResponse, res); - } - - try { - // Verifikasi token - console.log('Verifying Token:', token.substring(0, 10) + '...'); - - const decoded = jwt.verify(token, JWT_SECRET) as { - id: string; - username: string; - type?: string; - exp: number - }; - - console.log('Token Decoded:', { - id: decoded.id, - username: decoded.username, - type: decoded.type - }); - - // Tambahkan informasi pengguna ke request - (req as AuthenticatedRequest).user = { - id: decoded.id, - username: decoded.username, - type: decoded.type - }; - - next(); - } catch (error) { - console.error('Token Verification Error:', error); - - let message = 'Invalid token'; - - if (error instanceof jwt.TokenExpiredError) { - message = 'Token expired'; - } else if (error instanceof jwt.JsonWebTokenError) { - message = 'Invalid token signature'; - } - - const serviceResponse = new ServiceResponse( - ResponseStatus.Failed, - message, - null, - StatusCodes.UNAUTHORIZED - ); - return handleServiceResponse(serviceResponse, res); - } - }; -}; + ); +}; \ No newline at end of file diff --git a/src/common/middleware/rateLimiter.ts b/src/common/middleware/rateLimiter.ts index 5585c28..849b53a 100644 --- a/src/common/middleware/rateLimiter.ts +++ b/src/common/middleware/rateLimiter.ts @@ -6,7 +6,7 @@ import { logger } from '@/server'; const rateLimiter = rateLimit({ legacyHeaders: true, - limit: env.COMMON_RATE_LIMIT_MAX_REQUESTS ?? 100, + limit: env.COMMON_RATE_LIMIT_MAX_REQUESTS ?? 20, message: 'Too many requests, please try again later.', standardHeaders: true, windowMs: 15 * 60 * (env.COMMON_RATE_LIMIT_WINDOW_MS ?? 1000), diff --git a/src/index.ts b/src/index.ts index c894ba4..77ad5e9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ import { env } from '@/common/utils/envConfig'; import { app, logger } from '@/server'; - const port = env.PORT; const server = app.listen(port, () => { diff --git a/src/routes/excelGenerator/excelGeneratorRouter.ts b/src/routes/excelGenerator/excelGeneratorRouter.ts index 6fab892..d00c9c3 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -1,72 +1,33 @@ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; import * as ExcelJS from 'exceljs'; -import express, { NextFunction, Request, Response, Router } from 'express'; +import express, { Request, Response, Router } from 'express'; import fs from 'fs'; import { StatusCodes } from 'http-status-codes'; import cron from 'node-cron'; import path from 'path'; -import { z } from 'zod'; +import { EXCEL_EXPORTS_DIR } from '@/utils/paths'; import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; import { handleServiceResponse } from '@/common/utils/httpHandlers'; import { ExcelGeneratorRequestBodySchema, ExcelGeneratorResponseSchema } from './excelGeneratorModel'; -import { jwtMiddleware } from '@/common/middleware/jwtMiddleware'; export const COMPRESS = true; export const excelGeneratorRegistry = new OpenAPIRegistry(); excelGeneratorRegistry.register('ExcelGenerator', ExcelGeneratorResponseSchema); - - -// Registrasi path OpenAPI excelGeneratorRegistry.registerPath({ method: 'post', path: '/excel-generator/generate', tags: ['Excel Generator'], - security: [{ bearerAuth: [] }], // Tambahkan autentikasi request: { body: createApiRequestBody(ExcelGeneratorRequestBodySchema, 'application/json'), }, - responses: { - ...createApiResponse(ExcelGeneratorResponseSchema, 'Success'), - 400: { - description: 'Bad Request', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, - 401: { - description: 'Unauthorized', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, - 500: { - description: 'Internal Server Error', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, - }, + responses: createApiResponse(ExcelGeneratorResponseSchema, 'Success'), }); + // Create folder to contains generated files -const exportsDir = path.join(__dirname, '../../..', 'excel-exports'); -const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key'; +const exportsDir = EXCEL_EXPORTS_DIR; + // Ensure the exports directory exists if (!fs.existsSync(exportsDir)) { fs.mkdirSync(exportsDir, { recursive: true }); @@ -106,46 +67,34 @@ cron.schedule('0 * * * *', () => { }); }); -const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:8080'; - -// Ensure type safety for sheet data and excel configs -interface CellData { - type: 'static_value' | 'formula'; - value: string | number; -} - -interface ColumnConfig { - name: string; - type: 'string' | 'number' | 'boolean' | 'date' | 'percent' | 'currency'; - format?: string; -} - -interface TableConfig { - title?: string; - startCell: string; - rows: CellData[][]; - columns: ColumnConfig[]; - skipHeader?: boolean; -} +const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; interface SheetData { sheetName: string; - tables: TableConfig[]; + tables: { + title: string; + startCell: string; + rows: { + type: string; // static_value or formula, + value: string; + }[][]; + columns: { name: string; type: string; format: string }[]; // types that have format, number, percent, currency + skipHeader: boolean; + }[]; } interface ExcelConfig { - fontFamily?: string; - tableTitleFontSize?: number; - headerFontSize?: number; - fontSize?: number; - autoFitColumnWidth?: boolean; - autoFilter?: boolean; - borderStyle?: ExcelJS.BorderStyle | null; - wrapText?: boolean; + fontFamily: string; + tableTitleFontSize: number; // sesuaikan dengan nama yang digunakan di DEFAULT_EXCEL_CONFIGS + titleFontSize?: number; // tambahkan untuk backward compatibility + headerFontSize: number; + fontSize: number; + autoFitColumnWidth: boolean; + autoFilter: boolean; + borderStyle: ExcelJS.BorderStyle | null; + wrapText: boolean; } - -// Default configuration with full type annotations const DEFAULT_EXCEL_CONFIGS: ExcelConfig = { fontFamily: 'Calibri', tableTitleFontSize: 13, @@ -198,16 +147,7 @@ function autoFitColumns( } } -export function execGenExcelFuncs( - sheetsData: SheetData[], - excelConfigs: ExcelConfig = DEFAULT_EXCEL_CONFIGS -): Promise { - // Merge provided configs with defaults, ensuring complete coverage - const mergedConfigs: ExcelConfig = { - ...DEFAULT_EXCEL_CONFIGS, - ...excelConfigs - }; - +export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelConfig): string { const workbook = new ExcelJS.Workbook(); const borderConfigs = excelConfigs.borderStyle ? { @@ -379,103 +319,52 @@ export function execGenExcelFuncs( }); // Write the workbook to a file - const fileName = `maia-excel-${new Date().toISOString().replace(/\D/gi, '')}.xlsx`; + const fileName = `excel-file-${new Date().toISOString().replace(/\D/gi, '')}.xlsx`; const filePath = path.join(exportsDir, fileName); + workbook.xlsx + .writeFile(filePath) + .then(() => { + console.log('File has been written to', filePath); + }) + .catch((err) => { + console.error('Error writing Excel file', err); + }); - return new Promise((resolve, reject) => { - workbook.xlsx - .writeFile(filePath) - .then(() => { - console.log('File has been written to', filePath); - resolve(fileName); - }) - .catch((err) => { - console.error('Error writing Excel file', err); - reject(err); - }); - }); + return fileName; } export const excelGeneratorRouter: Router = (() => { const router = express.Router(); - // Middleware kustom untuk file statis dengan token - const tokenizedStaticMiddleware = (req: Request, res: Response, next: NextFunction) => { - // Tambahkan token ke query jika ada di path - const token = req.query.token; - if (token) { - return jwtMiddleware()(req, res, next); - } - next(); - }; - - // Gunakan middleware JWT untuk route downloads - router.use('/downloads', - tokenizedStaticMiddleware, - express.static(exportsDir, { - setHeaders: (res, filePath) => { - const isValidFile = filePath.startsWith(exportsDir); - if (!isValidFile) { - res.status(403).json({ - message: 'Access denied' - }); - } - }, - fallthrough: false - }) - ); + // Static route for downloading files - router.post('/generate', async (req: Request, res: Response) => { - const { sheetsData, excelConfigs } = req.body; // TODO: extract excel config object from request body - // Add explicit null/undefined check - if (!sheetsData || !Array.isArray(sheetsData) || sheetsData.length === 0) { - const validateServiceResponse = new ServiceResponse( - ResponseStatus.Failed, - '[Validation Error] Sheets data is required!', - 'Please make sure you have sent the excel sheets content generated from TypingMind.', - StatusCodes.BAD_REQUEST - ); - return handleServiceResponse(validateServiceResponse, res); - } - try { - // console.log('Received request body:', JSON.stringify(req.body, null, 2)); - const authHeader = req.headers.authorization; - const token = authHeader ? authHeader.split(' ')[1] : null; - const { - sheetsData, - excelConfigs = {} // Provide a default empty object - } = req.body || {}; - // Comprehensive input validation - if (!sheetsData || !Array.isArray(sheetsData) || sheetsData.length === 0) { - return handleServiceResponse( - new ServiceResponse( - ResponseStatus.Failed, - 'Validation Error', - 'Sheets data is required and must be a non-empty array', - StatusCodes.BAD_REQUEST - ), - res + router.post('/generate', async (_req: Request, res: Response) => { + const { sheetsData, excelConfigs = {} } = _req.body; + if (!sheetsData.length) { + const validateServiceResponse = new ServiceResponse( + ResponseStatus.Failed, + '[Validation Error] Sheets data is required!', + 'Please make sure you have sent the excel sheets content generated from TypingMind.', + StatusCodes.BAD_REQUEST ); + return handleServiceResponse(validateServiceResponse, res); } - // Create a completely safe configuration object with fallbacks - const validatedExcelConfigs: ExcelConfig = { - fontFamily: excelConfigs?.fontFamily ?? DEFAULT_EXCEL_CONFIGS.fontFamily, - tableTitleFontSize: excelConfigs?.titleFontSize ?? DEFAULT_EXCEL_CONFIGS.tableTitleFontSize, - headerFontSize: excelConfigs?.headerFontSize ?? DEFAULT_EXCEL_CONFIGS.headerFontSize, - fontSize: excelConfigs?.fontSize ?? DEFAULT_EXCEL_CONFIGS.fontSize, - autoFilter: excelConfigs?.autoFilter ?? DEFAULT_EXCEL_CONFIGS.autoFilter, - borderStyle: - excelConfigs?.borderStyle && excelConfigs.borderStyle !== 'none' - ? excelConfigs.borderStyle - : DEFAULT_EXCEL_CONFIGS.borderStyle, - wrapText: excelConfigs?.wrapText ?? DEFAULT_EXCEL_CONFIGS.wrapText, - autoFitColumnWidth: excelConfigs?.autoFitColumnWidth ?? DEFAULT_EXCEL_CONFIGS.autoFitColumnWidth, - }; - // Generate Excel file - const fileName = await execGenExcelFuncs(sheetsData, validatedExcelConfigs); + try { + const fileName = execGenExcelFuncs(sheetsData, { + fontFamily: excelConfigs.fontFamily ?? DEFAULT_EXCEL_CONFIGS.fontFamily, + tableTitleFontSize: excelConfigs.tableTitleFontSize ?? excelConfigs.titleFontSize ?? DEFAULT_EXCEL_CONFIGS.tableTitleFontSize, // handle both property names + headerFontSize: excelConfigs.headerFontSize ?? DEFAULT_EXCEL_CONFIGS.headerFontSize, + fontSize: excelConfigs.fontSize ?? DEFAULT_EXCEL_CONFIGS.fontSize, + autoFilter: excelConfigs.autoFilter ?? DEFAULT_EXCEL_CONFIGS.autoFilter, + borderStyle: excelConfigs.borderStyle === 'none' ? null : (excelConfigs.borderStyle ?? DEFAULT_EXCEL_CONFIGS.borderStyle), + wrapText: excelConfigs.wrapText ?? DEFAULT_EXCEL_CONFIGS.wrapText, + autoFitColumnWidth: excelConfigs.autoFitColumnWidth ?? DEFAULT_EXCEL_CONFIGS.autoFitColumnWidth, + }); + const authHeader = _req.headers.authorization; + const token = authHeader ? authHeader.split(' ')[1] : ''; const serviceResponse = new ServiceResponse( ResponseStatus.Success, 'File generated successfully', diff --git a/src/routes/healthCheck/healthCheckRouter.ts b/src/routes/healthCheck/healthCheckRouter.ts index bea00ba..64c8033 100644 --- a/src/routes/healthCheck/healthCheckRouter.ts +++ b/src/routes/healthCheck/healthCheckRouter.ts @@ -9,45 +9,18 @@ import { handleServiceResponse } from '@/common/utils/httpHandlers'; export const healthCheckRegistry = new OpenAPIRegistry(); -// Skema respons yang lebih detail -const HealthCheckResponseSchema = z.object({ - status: z.string(), - timestamp: z.string(), - uptime: z.number(), -}); - export const healthCheckRouter: Router = (() => { const router = express.Router(); healthCheckRegistry.registerPath({ method: 'get', path: '/health-check', - description: 'Health check endpoint', - security: [], // Tidak memerlukan autentikasi - responses: { - 200: { - description: 'Successful health check', - content: { - 'application/json': { - schema: HealthCheckResponseSchema, - }, - }, - }, - }, + tags: ['Health Check'], + responses: createApiResponse(z.null(), 'Success'), }); router.get('/', (_req: Request, res: Response) => { - const serviceResponse = new ServiceResponse( - ResponseStatus.Success, - 'Service is healthy', - { - status: 'OK', - timestamp: new Date().toISOString(), - uptime: process.uptime(), // Waktu uptime server - }, - StatusCodes.OK - ); - + const serviceResponse = new ServiceResponse(ResponseStatus.Success, 'Service is healthy', null, StatusCodes.OK); handleServiceResponse(serviceResponse, res); }); diff --git a/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts b/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts index ebf3e8a..7ff0136 100644 --- a/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts +++ b/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts @@ -1,3 +1,4 @@ +// powerpointGeneratorRouter.ts import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; import express, { NextFunction, Request, Response, Router } from 'express'; import fs from 'fs'; @@ -5,154 +6,96 @@ import { StatusCodes } from 'http-status-codes'; import cron from 'node-cron'; import path from 'path'; import pptxgen from 'pptxgenjs'; -import { z } from 'zod'; import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; import { handleServiceResponse } from '@/common/utils/httpHandlers'; -import jwt from 'jsonwebtoken'; -import { - PowerpointGeneratorRequestBodySchema, - PowerpointGeneratorResponseSchema -} from './powerpointGeneratorModel'; -import { AuthenticatedRequest, jwtMiddleware } from '@/common/middleware/jwtMiddleware'; +import { PowerpointGeneratorRequestBodySchema, PowerpointGeneratorResponseSchema } from './powerpointGeneratorModel'; export const COMPRESS = true; +import { POWERPOINT_EXPORTS_DIR } from '@/utils/paths'; // API Doc definition export const powerpointGeneratorRegistry = new OpenAPIRegistry(); powerpointGeneratorRegistry.register('PowerpointGenerator', PowerpointGeneratorResponseSchema); - -// Validasi skema request yang lebih ketat -const ValidatedPowerpointGeneratorRequestBodySchema = PowerpointGeneratorRequestBodySchema.refine( - (data) => { - // Tambahkan validasi kustom - return data.slides && data.slides.length > 0; - }, - { message: 'Slides must be a non-empty array' } -); - powerpointGeneratorRegistry.registerPath({ method: 'post', path: '/powerpoint-generator/generate', tags: ['Powerpoint Generator'], - security: [{ bearerAuth: [] }], // Tambahkan autentikasi request: { - body: createApiRequestBody(ValidatedPowerpointGeneratorRequestBodySchema, 'application/json'), - }, - responses: { - ...createApiResponse(PowerpointGeneratorResponseSchema, 'Success'), - 400: { - description: 'Bad Request', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, - 401: { - description: 'Unauthorized', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, - 500: { - description: 'Internal Server Error', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, + body: createApiRequestBody(PowerpointGeneratorRequestBodySchema, 'application/json'), }, + responses: createApiResponse(PowerpointGeneratorResponseSchema, 'Success'), }); // Create folder to contains generated files -const exportsDir = path.join(__dirname, '../../..', 'powerpoint-exports'); -const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key'; +const exportsDir = path.join(__dirname, '../../powerpoint-exports'); // Ensure the exports directory exists if (!fs.existsSync(exportsDir)) { - fs.mkdirSync(exportsDir, { recursive: true }); + fs.mkdirSync(exportsDir, { recursive: true, mode: 0o755 }); } -// Cron job to delete files older than 1 hour -cron.schedule('0 * * * *', () => { - const now = Date.now(); - const oneHour = 60 * 60 * 1000; - // Read the files in the exports directory - fs.readdir(exportsDir, (err, files) => { - if (err) { - console.error(`Error reading directory ${exportsDir}:`, err); - return; - } - - files.forEach((file) => { - const filePath = path.join(exportsDir, file); - fs.stat(filePath, (err, stats) => { - if (err) { - console.error(`Error getting stats for file ${filePath}:`, err); - return; - } - - // Check if the file is older than 1 hour - if (now - stats.mtime.getTime() > oneHour) { - fs.unlink(filePath, (err) => { - if (err) { - console.error(`Error deleting file ${filePath}:`, err); - } else { - console.log(`Deleted file: ${filePath}`); - } - }); +cron.schedule('0 * * * *', async () => { + try { + const now = Date.now(); + const oneHour = 60 * 60 * 1000; + + // Use promise-based fs operations + const files = await fs.promises.readdir(exportsDir); + + // Process files concurrently using Promise.all + await Promise.all( + files.map(async (file) => { + try { + const filePath = path.join(exportsDir, file); + const stats = await fs.promises.stat(filePath); + + if (now - stats.mtime.getTime() > oneHour) { + await fs.promises.unlink(filePath); + console.log(`Successfully deleted file: ${filePath}`); + } + } catch (err) { + console.error(`Error processing file ${file}:`, err); } - }); - }); - }); + }) + ); + } catch (err) { + console.error('Error in cleanup cron job:', err); + } }); -const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:8080'; +const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; // Define configurable options for layout, font size, and font family const defaultSlideConfig = { - layout: 'LAYOUT_WIDE', // Default: LAYOUT_WIDE, enum: LAYOUT_16x9 10 x 5.625 inches, LAYOUT_16x10 10 x 6.25 inches, LAYOUT_16x10 10 x 6.25 inches, LAYOUT_4x3 10 x 7.5 inches - titleFontSize: 44, // Emphasize the main topic in Title Slide - headerFontSize: 32, // The slide headers in the Content Slide - bodyFontSize: 22, // The main text font size - fontFamily: 'Calibri', // Default font family for the slide, Calibri, Arial - backgroundColor: '#FFFFFF', // Default background color - textColor: '#000000', // Text color - showFooter: false, // Display footer or not - showSlideNumber: false, // Display slide number or not - footerBackgroundColor: '#003B75', // Default background color - footerText: 'footer text', // Footer text content. - footerTextColor: '#FFFFFF', // Default footer color - footerFontSize: 10, // Default footer font size - showTableBorder: true, // Show table border or not - tableHeaderBackgroundColor: '#003B75', // Background of table header, // Dark blue background for headers - tableHeaderTextColor: '#FFFFFF', // Table header color - tableBorderThickness: 1, // pt: 1, // Border thickness - tableBorderColor: '#000000', // Black border - tableFontSize: 14, // Font size inside the table - tableTextColor: '#000000', // Text color inside the table + layout: 'LAYOUT_WIDE', + titleFontSize: 44, + headerFontSize: 32, + bodyFontSize: 22, + fontFamily: 'Calibri', + backgroundColor: '#FFFFFF', + textColor: '#000000', + showFooter: false, + showSlideNumber: false, + footerBackgroundColor: '#003B75', + footerText: 'footer text', + footerTextColor: '#FFFFFF', + footerFontSize: 10, + showTableBorder: true, + tableHeaderBackgroundColor: '#003B75', + tableHeaderTextColor: '#FFFFFF', + tableBorderThickness: 1, + tableBorderColor: '#000000', + tableFontSize: 14, + tableTextColor: '#000000', }; // Helper function to detect number, percent, or currency function detectType(value: string) { - // Regular expression patterns - const numberPattern = /^[+-]?\d+(\.\d+)?$/; // Matches general numbers - const percentPattern = /^[+-]?\d+(\.\d+)?%$/; // Matches percentages - const currencyPattern = /^[€$]\d+(\.\d+)?$/; // Matches currency values (e.g., $, €) + const numberPattern = /^[+-]?\d+(\.\d+)?$/; + const percentPattern = /^[+-]?\d+(\.\d+)?%$/; + const currencyPattern = /^[€$]\d+(\.\d+)?$/; if (currencyPattern.test(value)) { return 'currency'; @@ -161,11 +104,10 @@ function detectType(value: string) { } else if (numberPattern.test(value)) { return 'number'; } else { - return 'text'; // Default to text if no match + return 'text'; } } -// Helper function to get alignment based on detected type function getAlignment(type: string) { switch (type) { case 'number': @@ -184,8 +126,8 @@ function defineMasterSlides(pptx: any, config: any) { x: 0.0, y: 6.9, h: 0.6, - align: 'center', // Center text horizontally - valign: 'middle', // Center text vertically + align: 'center', + valign: 'middle', fontSize: config.footerFontSize, fontFace: config.fontFamily, color: config.footerTextColor, @@ -193,56 +135,52 @@ function defineMasterSlides(pptx: any, config: any) { } : undefined; - // Define the TITLE_SLIDE MasterSlide with vertically aligned header and subheader + // Define master slides pptx.defineSlideMaster({ title: 'TITLE_SLIDE', slideNumber: slideNumberConfig, objects: [ { - // Header (Section Title) placeholder: { options: { name: 'header', type: 'title', - x: '10%', // 10% from the left side of the slide for responsiveness - y: '20%', // Positioned 20% from the top of the slide - w: '80%', // Width adjusted to 80% of the slide width - h: 0.75, // Fixed height for the header - align: 'center', // Center-align the text horizontally - valign: 'middle', // Vertically align the text to the middle + x: '10%', + y: '20%', + w: '80%', + h: 0.75, + align: 'center', + valign: 'middle', margin: 0, fontSize: config.titleFontSize, - fontFace: config.fontFamily, // Set font face + fontFace: config.fontFamily, color: config.textColor, }, - text: '(title placeholer)', // Placeholder text for the title + text: '(title placeholer)', }, }, { - // Subheader (Subsection Title) placeholder: { options: { name: 'subheader', type: 'body', - x: '10%', // 10% from the left side of the slide for responsiveness - y: '35%', // Positioned 30% from the top, below the header - w: '80%', // Width adjusted to 80% of the slide width - h: 1.25, // Fixed height for the subheader - align: 'center', // Center-align the text horizontally - valign: 'middle', // Vertically align the text to the middle + x: '10%', + y: '35%', + w: '80%', + h: 1.25, + align: 'center', + valign: 'middle', margin: 0, fontSize: config.headerFontSize, - fontFace: config.fontFamily, // Set font face + fontFace: config.fontFamily, color: config.textColor, }, - text: '(subtitle placeholder)', // Placeholder text for the subheader + text: '(subtitle placeholder)', }, }, - // Footer background config.showFooter ? { rect: { x: 0.0, y: 6.9, w: '100%', h: 0.6, fill: { color: config.footerBackgroundColor } } } : {}, - // Footer Text config.showFooter ? { placeholder: { @@ -251,15 +189,15 @@ function defineMasterSlides(pptx: any, config: any) { type: 'body', x: 0.0, y: 6.9, - w: '100%', // Extend across the full width of the slide - h: 0.6, // Match the height of the footer background - align: 'center', // Center text horizontally - valign: 'middle', // Center text vertically - color: config.footerTextColor, // White text for contrast - fontSize: config.footerFontSize, // Suitable size for footer text - fontFace: config.fontFamily, // Set font face + w: '100%', + h: 0.6, + align: 'center', + valign: 'middle', + color: config.footerTextColor, + fontSize: config.footerFontSize, + fontFace: config.fontFamily, }, - text: config.footerText, // Default footer text + text: config.footerText, }, } : {}, @@ -268,13 +206,10 @@ function defineMasterSlides(pptx: any, config: any) { pptx.defineSlideMaster({ title: 'MASTER_SLIDE', - background: { - color: config.backgroundColor, - }, - margin: [0.5, 0.25, 1.0, 0.25], // top, left, bottom, right + background: { color: config.backgroundColor }, + margin: [0.5, 0.25, 1.0, 0.25], slideNumber: slideNumberConfig, objects: [ - // Header (Title) { placeholder: { options: { @@ -288,13 +223,12 @@ function defineMasterSlides(pptx: any, config: any) { align: 'center', valign: 'middle', color: config.textColor, - fontSize: config.headerFontSize, // Dynamically chosen for visibility - fontFace: config.fontFamily, // Set font face + fontSize: config.headerFontSize, + fontFace: config.fontFamily, }, - text: '(slide title placeholder)', // Default placeholder for title + text: '(slide title placeholder)', }, }, - // Content (Body) { placeholder: { options: { @@ -303,19 +237,17 @@ function defineMasterSlides(pptx: any, config: any) { x: '10%', y: '20%', w: '80%', - h: config.showFooter ? '60%' : '70%', // Responsive height + h: config.showFooter ? '60%' : '70%', color: config.textColor, - fontSize: config.bodyFontSize, // Suitable for body text - fontFace: config.fontFamily, // Set font face + fontSize: config.bodyFontSize, + fontFace: config.fontFamily, }, text: '(supports custom placeholder text!)', }, }, - // Footer background config.showFooter ? { rect: { x: 0.0, y: 6.9, w: '100%', h: 0.6, fill: { color: config.footerBackgroundColor } } } : {}, - // Footer Text config.showFooter ? { placeholder: { @@ -324,15 +256,15 @@ function defineMasterSlides(pptx: any, config: any) { type: 'body', x: 0.0, y: 6.9, - w: '100%', // Extend across the full width of the slide - h: 0.6, // Match the height of the footer background - align: 'center', // Center text horizontally - valign: 'middle', // Center text vertically - color: config.footerTextColor, // White text for contrast - fontSize: config.footerFontSize, // Suitable size for footer text - fontFace: config.fontFamily, // Set font face + w: '100%', + h: 0.6, + align: 'center', + valign: 'middle', + color: config.footerTextColor, + fontSize: config.footerFontSize, + fontFace: config.fontFamily, }, - text: config.footerText, // Default footer text + text: config.footerText, }, } : {}, @@ -341,16 +273,10 @@ function defineMasterSlides(pptx: any, config: any) { } async function execGenSlidesFuncs(slides: any[], config: any) { - // STEP 1: Instantiate new PptxGenJS object const pptx = new pptxgen(); - - // STEP 2: Set layout pptx.layout = config.layout; - - // STEP 3: Create Master Slides (from the old `pptxgen.masters.js` file - `gObjPptxMasters` items) defineMasterSlides(pptx, config); - // STEP 4: Run requested test slides.forEach((slideData, index) => { const { type, title, subtitle, chartContent, content = [] } = slideData; if (!type || !title) { @@ -358,18 +284,15 @@ async function execGenSlidesFuncs(slides: any[], config: any) { } if (type === 'title_slide') { - // Add a title slide const slide = pptx.addSlide({ masterName: 'TITLE_SLIDE' }); slide.addText(title, { placeholder: 'header' }); if (subtitle) { slide.addText(subtitle, { placeholder: 'subheader' }); } } else if (type === 'content_slide') { - // Add a content slide const slide = pptx.addSlide({ masterName: 'MASTER_SLIDE' }); slide.addText(title, { placeholder: 'header' }); - // Add content based on contentType if (content.length === 1) { slide.addText(content[0], { placeholder: 'body' }); } else if (content.length > 1) { @@ -377,17 +300,14 @@ async function execGenSlidesFuncs(slides: any[], config: any) { text: item, options: { bullet: true, valign: 'top' }, })); - slide.addText(bullets, { placeholder: 'body', valign: 'top' }); } else { throw new Error(`Invalid content length on slide ${index + 1}`); } } else if (type === 'table_slide') { - // Table Slide const slide = pptx.addSlide({ masterName: 'MASTER_SLIDE' }); slide.addText(title, { placeholder: 'header' }); - // Map content to tableData with alternating row colors and alignment const tableHeaders = content[0]; const tableData = [ tableHeaders.map((header: any) => ({ @@ -404,14 +324,13 @@ async function execGenSlidesFuncs(slides: any[], config: any) { row.map((cell: any) => { const cellType = detectType(cell); const align = getAlignment(cellType); - return { text: cell, options: { - fill: rowIndex % 2 === 0 ? 'E8F1FA' : 'DDEBF7', // Alternating row colors + fill: rowIndex % 2 === 0 ? 'E8F1FA' : 'DDEBF7', align, valign: 'middle', - color: config.tableTextColor, // Black text + color: config.tableTextColor, }, }; }) @@ -420,200 +339,156 @@ async function execGenSlidesFuncs(slides: any[], config: any) { const tableBorderConfigs = config.showTableBorder ? { - pt: config.tableBorderThickness, // Border thickness - color: config.tableBorderColor, // Black border + pt: config.tableBorderThickness, + color: config.tableBorderColor, } : undefined; slide.addTable(tableData, { - x: '10%', // Position aligned with placeholder + x: '10%', y: '20%', - w: '80%', // Table width - h: '60%', // Table height + w: '80%', + h: '60%', border: tableBorderConfigs, - fontSize: 14, // Font size for table text + fontSize: 14, placeholder: 'body', } as any); } else if (type === 'chart_slide' && chartContent) { - // Add a slide with the custom master slide const slide = pptx.addSlide({ masterName: 'MASTER_SLIDE' }); + slide.addText(title, { placeholder: 'header' }); - // Set the slide title - slide.addText(title, { - placeholder: 'header', - }); - - // Handle chart content based on chart type const { data: chartData, type: chartType } = chartContent; - // Default to line chart if no type is provided + + const chartOptions = { + x: '10%', + y: '20%', + w: '80%', + h: '60%', + showLegend: true, + placeholder: 'body', + } as any; + if (chartType === 'pie') { - slide.addChart(pptx.ChartType.pie, chartData, { - x: '10%', // Position aligned with placeholder - y: '20%', - w: '80%', // Table width - h: '60%', // Table height - showLegend: true, - showCategoryAxis: true, - showValueAxis: true, - showPercent: true, - dataLabelPosition: 'outside', - placeholder: 'body', - } as any); + chartOptions.showCategoryAxis = true; + chartOptions.showValueAxis = true; + chartOptions.showPercent = true; + chartOptions.dataLabelPosition = 'outside'; + slide.addChart(pptx.ChartType.pie, chartData, chartOptions); } else if (chartType === 'line') { - slide.addChart(pptx.ChartType.line, chartData, { - x: '10%', // Position aligned with placeholder - y: '20%', - w: '80%', // Table width - h: '60%', // Table height - showLegend: true, - showCategoryAxis: true, - showValueAxis: true, - dataLabelPosition: 'outside', - placeholder: 'body', - } as any); + chartOptions.showCategoryAxis = true; + chartOptions.showValueAxis = true; + chartOptions.dataLabelPosition = 'outside'; + slide.addChart(pptx.ChartType.line, chartData, chartOptions); } else if (chartType === 'bar') { - slide.addChart(pptx.ChartType.bar, chartData, { - x: '10%', // Position aligned with placeholder - y: '20%', - w: '80%', // Table width - h: '60%', // Table height - showLegend: true, - showCategoryAxis: true, - showValueAxis: true, - placeholder: 'body', - } as any); + chartOptions.showCategoryAxis = true; + chartOptions.showValueAxis = true; + slide.addChart(pptx.ChartType.bar, chartData, chartOptions); } else if (chartType === 'doughnut') { - slide.addChart(pptx.ChartType.doughnut, chartData, { - x: '10%', // Position aligned with placeholder - y: '20%', - w: '80%', // Table width - h: '60%', // Table height - showPercent: true, - showLegend: true, - placeholder: 'body', - } as any); + chartOptions.showPercent = true; + slide.addChart(pptx.ChartType.doughnut, chartData, chartOptions); } else { throw new Error(`Invalid chart type: ${chartType}`); } } }); - const fileName = `maia-presentation-${new Date().toISOString().replace(/\D/gi, '')}`; - const filePath = path.join(exportsDir, fileName); + const fileName = `your-presentation-${new Date().toISOString().replace(/\D/gi, '')}`; + const filePath = path.join(POWERPOINT_EXPORTS_DIR, fileName + '.pptx'); - await pptx.writeFile({ - fileName: filePath, - compression: COMPRESS, - }); + console.log('Debug execGenSlidesFuncs:'); + console.log('Saving file to:', filePath); + + try { + // Tunggu file selesai ditulis + await pptx.writeFile({ + fileName: filePath, + compression: COMPRESS, + }); + + // Verifikasi file telah dibuat + const fileExists = fs.existsSync(filePath); + console.log('File written successfully:', fileExists); + + if (!fileExists) { + throw new Error('File was not created successfully'); + } - return fileName + '.pptx'; + return fileName + '.pptx'; + } catch (error) { + console.error('Error writing file:', error); + throw error; + } } export const powerpointGeneratorRouter: Router = (() => { const router = express.Router(); - // Middleware kustom untuk file statis dengan token - const tokenizedStaticMiddleware = (req: Request, res: Response, next: NextFunction) => { - // Tambahkan token ke query jika ada di path - const token = req.query.token; - if (token) { - return jwtMiddleware()(req, res, next); - } - next(); - }; - - // Gunakan middleware JWT untuk route downloads - router.use('/downloads', - tokenizedStaticMiddleware, - express.static(exportsDir, { - setHeaders: (res, filePath) => { - const isValidFile = filePath.startsWith(exportsDir); - if (!isValidFile) { - res.status(403).json({ - message: 'Access denied' - }); - } - }, - fallthrough: false - }) - ); - - -router.post('/generate', async (req: Request, res: Response) => { - const { slides = [], slideConfig = {} } = req.body; - // Validasi input yang lebih komprehensif - if (!slides || !Array.isArray(slides) || slides.length === 0) { + + + router.post('/generate', async (_req: Request, res: Response) => { + + const { slides = [], slideConfig = {} } = _req.body; + if (!slides.length) { const validateServiceResponse = new ServiceResponse( ResponseStatus.Failed, - 'Validation Error: Presentation slides are required', - 'Please ensure you have sent valid slide content', + '[Validation Error] Presentation slides is required!', + 'Please make sure you have sent the slide content generated from TypingMind.', StatusCodes.BAD_REQUEST ); return handleServiceResponse(validateServiceResponse, res); } try { - // Log permintaan untuk debugging - console.log(`[Powerpoint Generator] Generating presentation with ${slides.length} slides`); - // Dapatkan token dari header Authorization - // Dapatkan token dari header Authorization - const authHeader = req.headers.authorization; - const token = authHeader ? authHeader.split(' ')[1] : null; - + if (!fs.existsSync(POWERPOINT_EXPORTS_DIR)) { + fs.mkdirSync(POWERPOINT_EXPORTS_DIR, { recursive: true }); + } const fileName = await execGenSlidesFuncs(slides, { - layout: slideConfig.layout === '' ? defaultSlideConfig.layout : slideConfig.layout, // Default: LAYOUT_WIDE, enum: LAYOUT_16x9 10 x 5.625 inches, LAYOUT_16x10 10 x 6.25 inches, LAYOUT_4x3 10 x 7.5 inches - titleFontSize: slideConfig.titleFontSize === 0 ? defaultSlideConfig.titleFontSize : slideConfig.titleFontSize, // Default: 52, Emphasize the main topic in Title Slide - headerFontSize: - slideConfig.headerFontSize === 0 ? defaultSlideConfig.headerFontSize : slideConfig.headerFontSize, // Default: 32, The slide headers in the Content Slide - bodyFontSize: slideConfig.bodyFontSize === 0 ? defaultSlideConfig.bodyFontSize : slideConfig.bodyFontSize, // Default: 24, The main text font size - fontFamily: slideConfig.fontFamily === '' ? defaultSlideConfig.fontFamily : slideConfig.fontFamily, // Default: 'Calibri', Default font family for the slide, Calibri, Arial - backgroundColor: - slideConfig.backgroundColor === '' ? defaultSlideConfig.backgroundColor : slideConfig.backgroundColor, // Default: '#FFFFFF', Default background color - textColor: slideConfig.textColor === '' ? defaultSlideConfig.textColor : slideConfig.textColor, // Default: '#000000', Text color - showFooter: slideConfig.showFooter ?? defaultSlideConfig.showFooter, // Default: false, Display footer or not - showSlideNumber: slideConfig.showSlideNumber ?? defaultSlideConfig.showSlideNumber, // Default: false, Display slide number or not - footerBackgroundColor: - slideConfig.footerBackgroundColor === '' - ? defaultSlideConfig.footerBackgroundColor - : slideConfig.footerBackgroundColor, // Default: '#003B75', Default footer background color - footerText: slideConfig.footerText === '' ? defaultSlideConfig.footerText : slideConfig.footerText, // Default: 'footer text', Footer text content. - footerTextColor: - slideConfig.footerTextColor === '' ? defaultSlideConfig.footerTextColor : slideConfig.footerTextColor, // Default: '#FFFFFF', Default footer text color - footerFontSize: - slideConfig.footerFontSize === 0 ? defaultSlideConfig.footerFontSize : slideConfig.footerFontSize, // Default: 10, Default footer font size - showTableBorder: slideConfig.showTableBorder ?? defaultSlideConfig.showTableBorder, // Default: true, Show table border or not - tableHeaderBackgroundColor: - slideConfig.tableHeaderBackgroundColor === '' - ? defaultSlideConfig.tableHeaderBackgroundColor - : slideConfig.tableHeaderBackgroundColor, // Default: '#003B75', Dark blue background for headers - tableHeaderTextColor: - slideConfig.tableHeaderTextColor === '' - ? defaultSlideConfig.tableHeaderTextColor - : slideConfig.tableHeaderTextColor, // Default: '#FFFFFF', Table header text color - tableBorderThickness: - slideConfig.tableBorderThickness === 0 - ? defaultSlideConfig.tableBorderThickness - : slideConfig.tableBorderThickness, // Default: 1 pt, Border thickness - tableBorderColor: - slideConfig.tableBorderColor === '' ? defaultSlideConfig.tableBorderColor : slideConfig.tableBorderColor, // Default: '#000000', Black border - tableFontSize: slideConfig.tableFontSize === 0 ? defaultSlideConfig.tableFontSize : slideConfig.tableFontSize, // Default: 14, Font size inside the table - tableTextColor: - slideConfig.tableTextColor === '' ? defaultSlideConfig.tableTextColor : slideConfig.tableTextColor, // Default: '#000000', Text color inside the table + layout: slideConfig.layout === '' ? defaultSlideConfig.layout : slideConfig.layout, + titleFontSize: slideConfig.titleFontSize === 0 ? defaultSlideConfig.titleFontSize : slideConfig.titleFontSize, + headerFontSize: slideConfig.headerFontSize === 0 ? defaultSlideConfig.headerFontSize : slideConfig.headerFontSize, + bodyFontSize: slideConfig.bodyFontSize === 0 ? defaultSlideConfig.bodyFontSize : slideConfig.bodyFontSize, + fontFamily: slideConfig.fontFamily === '' ? defaultSlideConfig.fontFamily : slideConfig.fontFamily, + backgroundColor: slideConfig.backgroundColor === '' ? defaultSlideConfig.backgroundColor : slideConfig.backgroundColor, + textColor: slideConfig.textColor === '' ? defaultSlideConfig.textColor : slideConfig.textColor, + showFooter: slideConfig.showFooter ?? defaultSlideConfig.showFooter, + showSlideNumber: slideConfig.showSlideNumber ?? defaultSlideConfig.showSlideNumber, + footerBackgroundColor: slideConfig.footerBackgroundColor === '' ? defaultSlideConfig.footerBackgroundColor : slideConfig.footerBackgroundColor, + footerText: slideConfig.footerText === '' ? defaultSlideConfig.footerText : slideConfig.footerText, + footerTextColor: slideConfig.footerTextColor === '' ? defaultSlideConfig.footerTextColor : slideConfig.footerTextColor, + footerFontSize: slideConfig.footerFontSize === 0 ? defaultSlideConfig.footerFontSize : slideConfig.footerFontSize, + showTableBorder: slideConfig.showTableBorder ?? defaultSlideConfig.showTableBorder, + tableHeaderBackgroundColor: slideConfig.tableHeaderBackgroundColor === '' ? defaultSlideConfig.tableHeaderBackgroundColor : slideConfig.tableHeaderBackgroundColor, + tableHeaderTextColor: slideConfig.tableHeaderTextColor === '' ? defaultSlideConfig.tableHeaderTextColor : slideConfig.tableHeaderTextColor, + tableBorderThickness: slideConfig.tableBorderThickness === 0 ? defaultSlideConfig.tableBorderThickness : slideConfig.tableBorderThickness, + tableBorderColor: slideConfig.tableBorderColor === '' ? defaultSlideConfig.tableBorderColor : slideConfig.tableBorderColor, + tableFontSize: slideConfig.tableFontSize === 0 ? defaultSlideConfig.tableFontSize : slideConfig.tableFontSize, + tableTextColor: slideConfig.tableTextColor === '' ? defaultSlideConfig.tableTextColor : slideConfig.tableTextColor, }); - - // Tambahkan token sebagai query parameter jika tersedia - const serviceResponse = new ServiceResponse( - ResponseStatus.Success, - 'File generated successfully', - { - // Tambahkan token ke URL download - downloadUrl: `${serverUrl}/powerpoint-generator/downloads/${fileName}?token=${token}`, - }, - StatusCodes.OK - ); - return handleServiceResponse(serviceResponse, res); + + // Log file creation + const fullFilePath = path.join(POWERPOINT_EXPORTS_DIR, fileName); + console.log('Generate Route Debug:'); + console.log('Full file path:', fullFilePath); + console.log('File exists after generation:', fs.existsSync(fullFilePath)); + console.log('Directory contents:', fs.readdirSync(POWERPOINT_EXPORTS_DIR)); + + // Verify file exists before sending response + if (!fs.existsSync(fullFilePath)) { + throw new Error('File generation failed - file not found after generation'); + } + + const authHeader = _req.headers.authorization; + const token = authHeader ? authHeader.split(' ')[1] : ''; + + const serviceResponse = new ServiceResponse( + ResponseStatus.Success, + 'File generated successfully', + { + downloadUrl: `${serverUrl}/powerpoint-generator/downloads/${fileName}?token=${token}`, + }, + StatusCodes.OK + ); + return handleServiceResponse(serviceResponse, res); } catch (error) { - // Log error untuk debugging - console.error(`[Powerpoint Generator] Error:`, error); const errorMessage = (error as Error).message; let responseObject = ''; if (errorMessage.includes('')) { @@ -629,4 +504,4 @@ router.post('/generate', async (req: Request, res: Response) => { } }); return router; -})(); +})(); \ No newline at end of file diff --git a/src/routes/webPageReader/webPageReaderRouter.ts b/src/routes/webPageReader/webPageReaderRouter.ts index 8ac722f..ee4adf0 100644 --- a/src/routes/webPageReader/webPageReaderRouter.ts +++ b/src/routes/webPageReader/webPageReaderRouter.ts @@ -11,7 +11,6 @@ import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse import { handleServiceResponse } from '@/common/utils/httpHandlers'; import { WebPageReaderRequestParamSchema, WebPageReaderResponseSchema } from './webPageReaderModel'; -import { z } from 'zod'; export const articleReaderRegistry = new OpenAPIRegistry(); articleReaderRegistry.register('Web Page Reader', WebPageReaderResponseSchema); @@ -67,38 +66,12 @@ export const webPageReaderRouter: Router = (() => { method: 'get', path: '/web-page-reader/get-content', tags: ['Web Page Reader'], - security: [{ bearerAuth: [] }], // Tambahkan autentikasi request: { query: WebPageReaderRequestParamSchema, }, - responses: { - ...createApiResponse(WebPageReaderResponseSchema, 'Success'), - 401: { - description: 'Unauthorized', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, - 400: { - description: 'Bad Request', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, - }, + responses: createApiResponse(WebPageReaderResponseSchema, 'Success'), }); - router.get('/get-content', async (_req: Request, res: Response) => { const { url } = _req.query; diff --git a/src/routes/wordGenerator/wordGeneratorRouter.ts b/src/routes/wordGenerator/wordGeneratorRouter.ts index 5d69e92..91c2d12 100644 --- a/src/routes/wordGenerator/wordGeneratorRouter.ts +++ b/src/routes/wordGenerator/wordGeneratorRouter.ts @@ -19,80 +19,33 @@ import { TextRun, WidthType, } from 'docx'; -import express, { NextFunction, Request, Response, Router } from 'express'; +import express, { Request, Response, Router } from 'express'; import fs from 'fs'; import { StatusCodes } from 'http-status-codes'; import cron from 'node-cron'; import path from 'path'; -import { z } from 'zod'; +import { WORD_EXPORTS_DIR } from '@/utils/paths'; import { createApiRequestBody } from '@/api-docs/openAPIRequestBuilders'; import { createApiResponse } from '@/api-docs/openAPIResponseBuilders'; import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse'; import { handleServiceResponse } from '@/common/utils/httpHandlers'; import { WordGeneratorRequestBodySchema, WordGeneratorResponseSchema } from './wordGeneratorModel'; -import { jwtMiddleware } from '@/common/middleware/jwtMiddleware'; export const COMPRESS = true; export const wordGeneratorRegistry = new OpenAPIRegistry(); wordGeneratorRegistry.register('WordGenerator', WordGeneratorResponseSchema); -// Validasi skema request yang lebih ketat -const ValidatedWordGeneratorRequestBodySchema = WordGeneratorRequestBodySchema.refine( - (data) => { - // Tambahkan validasi kustom - return data.sections && data.sections.length > 0 && data.title && data.title.trim() !== ''; - }, - { message: 'Sections must be a non-empty array and title must be provided' } -); - wordGeneratorRegistry.registerPath({ method: 'post', path: '/word-generator/generate', tags: ['Word Generator'], - security: [{ bearerAuth: [] }], // Tambahkan autentikasi request: { - body: createApiRequestBody(ValidatedWordGeneratorRequestBodySchema, 'application/json'), - }, - responses: { - ...createApiResponse(WordGeneratorResponseSchema, 'Success'), - 400: { - description: 'Bad Request', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, - 401: { - description: 'Unauthorized', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, - 500: { - description: 'Internal Server Error', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, + body: createApiRequestBody(WordGeneratorRequestBodySchema, 'application/json'), }, + responses: createApiResponse(WordGeneratorResponseSchema, 'Success'), }); // Create folder to contains generated files -const exportsDir = path.join(__dirname, '../../..', 'word-exports'); -const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key'; +const exportsDir = WORD_EXPORTS_DIR; // Ensure the exports directory exists if (!fs.existsSync(exportsDir)) { fs.mkdirSync(exportsDir, { recursive: true }); @@ -132,7 +85,7 @@ cron.schedule('0 * * * *', () => { }); }); -const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:8080'; +const serverUrl = process.env.RENDER_EXTERNAL_URL || 'http://localhost:3000'; const FONT_CONFIG = { size: 12, // Font size in point @@ -622,7 +575,7 @@ async function execGenWordFuncs( footnotes: footnoteConfig, // TODO: Enhance footnote }); - const fileName = `word-maia-${new Date().toISOString().replace(/\D/gi, '')}.docx`; + const fileName = `word-file-${new Date().toISOString().replace(/\D/gi, '')}.docx`; const filePath = path.join(exportsDir, fileName); // Create and save the document @@ -635,34 +588,11 @@ async function execGenWordFuncs( export const wordGeneratorRouter: Router = (() => { const router = express.Router(); - // Middleware kustom untuk file statis dengan token - const tokenizedStaticMiddleware = (req: Request, res: Response, next: NextFunction) => { - // Tambahkan token ke query jika ada di path - const token = req.query.token; - if (token) { - return jwtMiddleware()(req, res, next); - } - next(); - }; // Static route for downloading files - // Gunakan middleware JWT untuk route downloads - router.use('/downloads', - tokenizedStaticMiddleware, - express.static(exportsDir, { - setHeaders: (res, filePath) => { - const isValidFile = filePath.startsWith(exportsDir); - if (!isValidFile) { - res.status(403).json({ - message: 'Access denied' - }); - } - }, - fallthrough: false - }) - ); - router.post('/generate', async (req: Request, res: Response) => { - const { title, sections = [], header, footer, wordConfig = {} } = req.body; + + router.post('/generate', async (_req: Request, res: Response) => { + const { title, sections = [], header, footer, wordConfig = {} } = _req.body; if (!sections.length) { const validateServiceResponse = new ServiceResponse( ResponseStatus.Failed, @@ -674,9 +604,6 @@ export const wordGeneratorRouter: Router = (() => { } try { - console.log(`[Word Generator] Generating Word with ${sections.length}`); - const authHeader = req.headers.authorization; - const token = authHeader ? authHeader.split(' ')[1] : null; const wordConfigs = { numberingReference: wordConfig.showNumberingInHeader ? wordConfig.numberingReference : '', showPageNumber: wordConfig.showPageNumber ?? false, @@ -697,6 +624,10 @@ export const wordGeneratorRouter: Router = (() => { }, wordConfigs ); + + + const authHeader = _req.headers.authorization; + const token = authHeader ? authHeader.split(' ')[1] : ''; const serviceResponse = new ServiceResponse( ResponseStatus.Success, 'File generated successfully', diff --git a/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts b/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts index ca6c46d..64417f2 100644 --- a/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts +++ b/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts @@ -8,7 +8,6 @@ import { ResponseStatus, ServiceResponse } from '@/common/models/serviceResponse import { handleServiceResponse } from '@/common/utils/httpHandlers'; import { YoutubeTranscriptRequestParamSchema, YoutubeTranscriptResponseSchema } from './youtubeTranscriptModel'; -import { z } from 'zod'; export const youtubeTranscriptRegistry = new OpenAPIRegistry(); youtubeTranscriptRegistry.register('YoutubeTranscript', YoutubeTranscriptResponseSchema); @@ -20,35 +19,10 @@ export const youtubeTranscriptRouter: Router = (() => { method: 'get', path: '/youtube-transcript/get-transcript', tags: ['Youtube Transcript'], - security: [{ bearerAuth: [] }], // Tambahkan autentikasi request: { query: YoutubeTranscriptRequestParamSchema, }, - responses: { - ...createApiResponse(YoutubeTranscriptResponseSchema, 'Success'), - 401: { - description: 'Unauthorized', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, - 400: { - description: 'Bad Request', - content: { - 'application/json': { - schema: z.object({ - message: z.string(), - error: z.string().optional(), - }), - }, - }, - }, - }, + responses: createApiResponse(YoutubeTranscriptResponseSchema, 'Success'), }); router.get('/get-transcript', async (_req: Request, res: Response) => { diff --git a/src/server.ts b/src/server.ts index c5f9520..f94e9dd 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,71 +2,190 @@ import bodyParser from 'body-parser'; import cors from 'cors'; import express, { Express } from 'express'; import helmet from 'helmet'; +import jwt from 'jsonwebtoken'; import { pino } from 'pino'; -import dotenv from 'dotenv'; - -// Load environment variables -dotenv.config(); - +import { Request, Response, NextFunction } from 'express'; import { openAPIRouter } from '@/api-docs/openAPIRouter'; import errorHandler from '@/common/middleware/errorHandler'; -import rateLimiter from '@/common/middleware/rateLimiter'; +// import rateLimiter from '@/common/middleware/rateLimiter'; import requestLogger from '@/common/middleware/requestLogger'; -import { jwtMiddleware } from '@/common/middleware/jwtMiddleware'; import { healthCheckRouter } from '@/routes/healthCheck/healthCheckRouter'; +import { authRouter } from './auth/authRouter'; -// Import routers import { excelGeneratorRouter } from './routes/excelGenerator/excelGeneratorRouter'; import { notionDatabaseRouter } from './routes/notionDatabase/notionDatabaseRouter'; import { powerpointGeneratorRouter } from './routes/powerpointGenerator/powerpointGeneratorRouter'; import { webPageReaderRouter } from './routes/webPageReader/webPageReaderRouter'; import { wordGeneratorRouter } from './routes/wordGenerator/wordGeneratorRouter'; import { youtubeTranscriptRouter } from './routes/youtubeTranscript/youtubeTranscriptRouter'; -import authRouter from './auth/authRouter'; - +import path from 'path'; +import fs from 'fs'; const logger = pino({ name: 'server start' }); const app: Express = express(); -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); -// Pastikan secret key ada -if (!process.env.JWT_SECRET) { - console.error('JWT_SECRET is not defined in environment variables'); - process.exit(1); -} +import { POWERPOINT_EXPORTS_DIR } from '@/utils/paths'; +import { WORD_EXPORTS_DIR, EXCEL_EXPORTS_DIR } from '@/utils/paths'; + +[WORD_EXPORTS_DIR, EXCEL_EXPORTS_DIR].forEach(dir => { + if (!fs.existsSync(dir)) { + console.log('Creating directory:', dir); + fs.mkdirSync(dir, { recursive: true }); + } +}); + +// Set the application to trust the reverse proxy +app.set('trust proxy', true); // Middlewares app.use(cors()); app.use(helmet()); // app.use(rateLimiter); app.use(bodyParser.json()); +app.use((req, res, next) => { + res.removeHeader('X-Frame-Options'); + res.removeHeader('Content-Security-Policy'); + next(); +}); + +// Request logging app.use(requestLogger()); -// Public Routes +// Public routes app.use('/health-check', healthCheckRouter); -app.use('/auth', authRouter); // Tambahkan route autentikasi app.use('/images', express.static('public/images')); +app.use('/auth', authRouter); +const downloadAuthMiddleware = (req: Request, res: Response, next: NextFunction) => { + try { + const queryToken = req.query.token; + const authHeader = req.headers.authorization; + const token = queryToken || (authHeader ? authHeader.split(' ')[1] : null); + + if (!token) { + return res.status(401).json({ message: 'Authentication required' }); + } + const secret = process.env.JWT_SECRET || 'your-secret-key'; + jwt.verify(token as string, secret); + next(); + } catch (error) { + console.error('Auth error:', error); + return res.status(401).json({ message: 'Authentication failed' }); + } +}; -app.use([ - '/youtube-transcript', - '/web-page-reader', - '/powerpoint-generator/generate', - '/word-generator', - '/excel-generator', - '/notion-database' -], jwtMiddleware()); +app.use('/powerpoint-generator/downloads', + downloadAuthMiddleware, + (req, res, next) => { + const fileName = req.path.slice(1); + const filePath = path.join(POWERPOINT_EXPORTS_DIR, fileName); + + console.log('Download Middleware Debug:'); + console.log('Request path:', req.path); + console.log('File name:', fileName); + console.log('Full file path:', filePath); + console.log('Directory contents:', fs.readdirSync(POWERPOINT_EXPORTS_DIR)); + + if (!fileName || !fs.existsSync(filePath)) { + return res.status(404).json({ + message: 'File not found', + debug: { + path: filePath, + exists: fs.existsSync(filePath), + dirContents: fs.readdirSync(POWERPOINT_EXPORTS_DIR) + } + }); + } + next(); + }, + express.static(POWERPOINT_EXPORTS_DIR) +); + +// Add download routes for Word +app.use('/word-generator/downloads', + downloadAuthMiddleware, + (req, res, next) => { + const fileName = req.path.slice(1); + const filePath = path.join(WORD_EXPORTS_DIR, fileName); + + console.log('Word Download Debug:'); + console.log('Request path:', req.path); + console.log('File name:', fileName); + console.log('Full file path:', filePath); + console.log('Directory contents:', fs.readdirSync(WORD_EXPORTS_DIR)); + + if (!fileName || !fs.existsSync(filePath)) { + return res.status(404).json({ + message: 'File not found', + debug: { + path: filePath, + exists: fs.existsSync(filePath), + dirContents: fs.readdirSync(WORD_EXPORTS_DIR) + } + }); + } + next(); + }, + express.static(WORD_EXPORTS_DIR) +); + +app.use('/excel-generator/downloads', + downloadAuthMiddleware, + (req, res, next) => { + const fileName = req.path.slice(1); + const filePath = path.join(EXCEL_EXPORTS_DIR, fileName); + + console.log('Excel Download Debug:'); + console.log('Request path:', req.path); + console.log('File name:', fileName); + console.log('Full file path:', filePath); + console.log('Directory contents:', fs.readdirSync(EXCEL_EXPORTS_DIR)); + + if (!fileName || !fs.existsSync(filePath)) { + return res.status(404).json({ + message: 'File not found', + debug: { + path: filePath, + exists: fs.existsSync(filePath), + dirContents: fs.readdirSync(EXCEL_EXPORTS_DIR) + } + }); + } + next(); + }, + express.static(EXCEL_EXPORTS_DIR) +); + +// Protected API routes +const protectedRoutes = express.Router(); +protectedRoutes.use(async (req, res, next) => { + try { + const authHeader = req.headers.authorization; + if (!authHeader) { + return res.status(401).json({ message: 'Authentication' }); + } + + const token = authHeader.split(' ')[1]; + const secret = process.env.JWT_SECRET || 'your-secret-key'; + + jwt.verify(token, secret); + next(); + } catch (error) { + console.error('Auth error:', error); + return res.status(401).json({ message: 'Authentication failed' }); + } +}); +// Apply protected routes +app.use('/youtube-transcript', protectedRoutes, youtubeTranscriptRouter); +app.use('/web-page-reader', protectedRoutes, webPageReaderRouter); +app.use('/powerpoint-generator', protectedRoutes, powerpointGeneratorRouter); +app.use('/word-generator', protectedRoutes, wordGeneratorRouter); +app.use('/excel-generator', protectedRoutes, excelGeneratorRouter); +app.use('/notion-database', protectedRoutes, notionDatabaseRouter); -// Protected Routes -app.use('/youtube-transcript', youtubeTranscriptRouter); -app.use('/web-page-reader', webPageReaderRouter); -app.use('/powerpoint-generator', powerpointGeneratorRouter); -app.use('/word-generator', wordGeneratorRouter); -app.use('/excel-generator', excelGeneratorRouter); -app.use('/notion-database', notionDatabaseRouter); +// Root route (should be last) app.get('/', (req, res) => { - res.status(200).json({ status: 'ok' }); + res.json({ message: 'API is running' }); }); // Swagger UI app.use(openAPIRouter); @@ -74,4 +193,4 @@ app.use(openAPIRouter); // Error handlers app.use(errorHandler()); -export { app, logger }; +export { app, logger }; \ No newline at end of file diff --git a/src/utils/paths.ts b/src/utils/paths.ts new file mode 100644 index 0000000..3e62a3d --- /dev/null +++ b/src/utils/paths.ts @@ -0,0 +1,8 @@ +import path from 'path'; + +// Get the root directory of the project +const ROOT_DIR = path.resolve(__dirname, '../../'); + +export const POWERPOINT_EXPORTS_DIR = path.join(ROOT_DIR, 'powerpoint-exports'); +export const WORD_EXPORTS_DIR = path.join(ROOT_DIR, 'word-exports'); +export const EXCEL_EXPORTS_DIR = path.join(ROOT_DIR, 'excel-exports'); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 5450097..90292a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,4 +24,4 @@ }, "exclude": ["node_modules", "dist"], "include": ["src/**/*.ts"] -} \ No newline at end of file +}