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 diff --git a/Dockerfile b/Dockerfile index 3a1efbb..4759f1f 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..47f068d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,15 @@ "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", "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", @@ -29,13 +33,15 @@ "http-status-codes": "^2.3.0", "jsdom": "^26.0.0", "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", @@ -44,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", @@ -1278,6 +1285,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 +1303,64 @@ "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==", + "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/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "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", @@ -1978,6 +2053,15 @@ "parse5": "^7.0.0" } }, + "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/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2032,6 +2116,37 @@ "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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", @@ -2127,6 +2242,11 @@ "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==" + }, "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 +3098,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 +3377,21 @@ "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==" + }, + "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==", + "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 +4574,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 +7080,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 +7254,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 +7314,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 +7421,11 @@ "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==" + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -7393,6 +7601,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 +7622,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 +7647,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 +7672,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 +8676,40 @@ "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==", + "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==", + "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 +8796,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 +9509,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 +9917,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 +9979,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 +11615,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 +11807,14 @@ "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==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -12717,9 +13013,9 @@ } }, "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==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index ab5fde9..5215105 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,15 @@ "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", "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", @@ -38,13 +42,15 @@ "http-status-codes": "^2.3.0", "jsdom": "^26.0.0", "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", @@ -53,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", @@ -81,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/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.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 new file mode 100644 index 0000000..da6e784 --- /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: '5h' }, + }), + ], + 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..6d7dfde --- /dev/null +++ b/src/auth/authRouter.ts @@ -0,0 +1,78 @@ +// authRouter.ts +import express from 'express'; +import jwt from 'jsonwebtoken'; + +const router = express.Router(); + +router.post('/login', (req, res) => { + const { username, password } = req.body; + + // Basic validation to ensure credentials are provided + if (!username || !password) { + return res.status(400).json({ + message: 'Username and password are required' + }); + } + + // 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' + }; + + try { + const token = jwt.sign( + payload, + process.env.JWT_SECRET || 'your-secret-key', + { + expiresIn: '5h', + algorithm: 'HS256' + } + ); + + // 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) { + return res.status(401).json({ + valid: false, + message: 'Invalid token' + }); + } +}); + +export const authRouter = router; \ No newline at end of file diff --git a/src/auth/jwt.guard.ts b/src/auth/jwt.guard.ts new file mode 100644 index 0000000..b255b07 --- /dev/null +++ b/src/auth/jwt.guard.ts @@ -0,0 +1,41 @@ +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 new file mode 100644 index 0000000..8b807ab --- /dev/null +++ b/src/auth/jwt.strategy.ts @@ -0,0 +1,33 @@ +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() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: process.env.JWT_SECRET || 'your-secret-key', + }); + } + + async validate(payload: JwtPayload) { + // Validasi payload + if (!payload.sub || !payload.email) { + throw new UnauthorizedException('Invalid token payload'); + } + return { + userId: payload.sub, + email: payload.email + }; + } +} diff --git a/src/common/middleware/jwtMiddleware.ts b/src/common/middleware/jwtMiddleware.ts new file mode 100644 index 0000000..7e79dbf --- /dev/null +++ b/src/common/middleware/jwtMiddleware.ts @@ -0,0 +1,19 @@ +import { Request, Response, NextFunction } from 'express'; +import { JwtAuthGuard } from '../../auth/jwt.guard'; + +const jwtAuthGuard = new JwtAuthGuard(); + +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 }); + } + ); +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts old mode 100755 new mode 100644 index c894ba4..77ad5e9 --- 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/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..d00c9c3 100644 --- a/src/routes/excelGenerator/excelGeneratorRouter.ts +++ b/src/routes/excelGenerator/excelGeneratorRouter.ts @@ -5,7 +5,7 @@ import fs from 'fs'; import { StatusCodes } from 'http-status-codes'; import cron from 'node-cron'; import path from 'path'; - +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'; @@ -26,7 +26,7 @@ excelGeneratorRegistry.registerPath({ }); // Create folder to contains generated files -const exportsDir = path.join(__dirname, '../../..', 'excel-exports'); +const exportsDir = EXCEL_EXPORTS_DIR; // Ensure the exports directory exists if (!fs.existsSync(exportsDir)) { @@ -85,12 +85,13 @@ interface SheetData { interface ExcelConfig { fontFamily: string; - tableTitleFontSize: number; + 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; // thin, double, dashed, thick + borderStyle: ExcelJS.BorderStyle | null; wrapText: boolean; } @@ -186,7 +187,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 @@ -336,10 +337,10 @@ export function execGenExcelFuncs(sheetsData: SheetData[], excelConfigs: ExcelCo export const excelGeneratorRouter: 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 { sheetsData, excelConfigs } = _req.body; // TODO: extract excel config object from request body + const { sheetsData, excelConfigs = {} } = _req.body; if (!sheetsData.length) { const validateServiceResponse = new ServiceResponse( ResponseStatus.Failed, @@ -353,23 +354,22 @@ export const excelGeneratorRouter: Router = (() => { try { const fileName = execGenExcelFuncs(sheetsData, { fontFamily: excelConfigs.fontFamily ?? DEFAULT_EXCEL_CONFIGS.fontFamily, - tableTitleFontSize: excelConfigs.titleFontSize ?? DEFAULT_EXCEL_CONFIGS.tableTitleFontSize, + 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 || excelConfigs.borderStyle !== 'none' - ? excelConfigs.borderStyle - : DEFAULT_EXCEL_CONFIGS.borderStyle, + 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', { - downloadUrl: `${serverUrl}/excel-generator/downloads/${fileName}`, + downloadUrl: `${serverUrl}/excel-generator/downloads/${fileName}?token=${token}`, }, StatusCodes.OK ); diff --git a/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts b/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts index 8d0f543..7ff0136 100644 --- a/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts +++ b/src/routes/powerpointGenerator/powerpointGeneratorRouter.ts @@ -1,5 +1,6 @@ +// powerpointGeneratorRouter.ts 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'; @@ -14,6 +15,7 @@ import { handleServiceResponse } from '@/common/utils/httpHandlers'; 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); @@ -28,78 +30,72 @@ powerpointGeneratorRegistry.registerPath({ }); // Create folder to contains generated files -const exportsDir = path.join(__dirname, '../../..', 'powerpoint-exports'); +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: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'; @@ -108,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': @@ -131,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, @@ -140,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: { @@ -198,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, }, } : {}, @@ -215,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: { @@ -235,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: { @@ -250,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: { @@ -271,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, }, } : {}, @@ -288,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) { @@ -305,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) { @@ -324,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) => ({ @@ -351,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, }, }; }) @@ -367,78 +339,53 @@ 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}`); } @@ -446,22 +393,39 @@ async function execGenSlidesFuncs(slides: any[], config: any) { }); const fileName = `your-presentation-${new Date().toISOString().replace(/\D/gi, '')}`; - const filePath = path.join(exportsDir, fileName); + 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(); - // 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) { const validateServiceResponse = new ServiceResponse( @@ -474,55 +438,56 @@ export const powerpointGeneratorRouter: Router = (() => { } try { + 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, }); - const serviceResponse = new ServiceResponse( - ResponseStatus.Success, - 'File generated successfully', - { - downloadUrl: `${serverUrl}/powerpoint-generator/downloads/${fileName}`, - }, - 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) { const errorMessage = (error as Error).message; let responseObject = ''; @@ -539,4 +504,4 @@ export const powerpointGeneratorRouter: Router = (() => { } }); return router; -})(); +})(); \ No newline at end of file diff --git a/src/routes/wordGenerator/wordGeneratorRouter.ts b/src/routes/wordGenerator/wordGeneratorRouter.ts index 8823b0a..91c2d12 100644 --- a/src/routes/wordGenerator/wordGeneratorRouter.ts +++ b/src/routes/wordGenerator/wordGeneratorRouter.ts @@ -24,7 +24,7 @@ import fs from 'fs'; import { StatusCodes } from 'http-status-codes'; import cron from 'node-cron'; import path from 'path'; - +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'; @@ -45,7 +45,7 @@ wordGeneratorRegistry.registerPath({ }); // Create folder to contains generated files -const exportsDir = path.join(__dirname, '../../..', 'word-exports'); +const exportsDir = WORD_EXPORTS_DIR; // Ensure the exports directory exists if (!fs.existsSync(exportsDir)) { fs.mkdirSync(exportsDir, { recursive: true }); @@ -589,7 +589,7 @@ async function execGenWordFuncs( export const wordGeneratorRouter: 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 { title, sections = [], header, footer, wordConfig = {} } = _req.body; @@ -624,11 +624,15 @@ 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', { - 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..64417f2 100644 --- a/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts +++ b/src/routes/youtubeTranscript/youtubeTranscriptRouter.ts @@ -27,7 +27,7 @@ export const youtubeTranscriptRouter: Router = (() => { 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..f94e9dd 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,13 +2,15 @@ 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 { 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 { healthCheckRouter } from '@/routes/healthCheck/healthCheckRouter'; +import { authRouter } from './auth/authRouter'; import { excelGeneratorRouter } from './routes/excelGenerator/excelGeneratorRouter'; import { notionDatabaseRouter } from './routes/notionDatabase/notionDatabaseRouter'; @@ -16,39 +18,179 @@ import { powerpointGeneratorRouter } from './routes/powerpointGenerator/powerpoi import { webPageReaderRouter } from './routes/webPageReader/webPageReaderRouter'; import { wordGeneratorRouter } from './routes/wordGenerator/wordGeneratorRouter'; import { youtubeTranscriptRouter } from './routes/youtubeTranscript/youtubeTranscriptRouter'; +import path from 'path'; +import fs from 'fs'; const logger = pino({ name: 'server start' }); const app: Express = express(); +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({ origin: env.CORS_ORIGIN, credentials: true })); app.use(cors()); app.use(helmet()); -app.use(rateLimiter); +// 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('/images', express.static('public/images')); -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.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('/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); + +// Root route (should be last) +app.get('/', (req, res) => { + res.json({ message: 'API is running' }); +}); // Swagger UI 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 73b6047..90292a6 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"] }