v0.4.1: Code-driven OpenAPI docs via swagger-jsdoc
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
- Add swagger-jsdoc dependency for auto-generating OpenAPI spec from JSDoc - Add JSDoc @openapi annotations to all route handlers - Create scripts/generate-openapi.mjs build step - OpenAPI spec now auto-generated from code — no manual JSON editing - All 13 endpoints documented with full parameters - New demo endpoints documented, signup marked as deprecated - Updated info description: demo-first, no free tier references - Dockerfile updated to run openapi generation during build - Build script updated: npm run build generates spec before compile
This commit is contained in:
parent
53755d6093
commit
792e2d9142
12 changed files with 1931 additions and 305 deletions
|
|
@ -24,6 +24,7 @@ RUN npx tsc
|
||||||
RUN npm prune --omit=dev
|
RUN npm prune --omit=dev
|
||||||
COPY scripts/ scripts/
|
COPY scripts/ scripts/
|
||||||
COPY public/ public/
|
COPY public/ public/
|
||||||
|
RUN node scripts/generate-openapi.mjs
|
||||||
RUN node scripts/build-html.cjs
|
RUN node scripts/build-html.cjs
|
||||||
RUN rm -f public/swagger-ui && ln -s /app/node_modules/swagger-ui-dist public/swagger-ui
|
RUN rm -f public/swagger-ui && ln -s /app/node_modules/swagger-ui-dist public/swagger-ui
|
||||||
|
|
||||||
|
|
|
||||||
294
package-lock.json
generated
294
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "docfast-api",
|
"name": "docfast-api",
|
||||||
"version": "0.2.1",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "docfast-api",
|
"name": "docfast-api",
|
||||||
"version": "0.2.1",
|
"version": "0.4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"puppeteer": "^24.0.0",
|
"puppeteer": "^24.0.0",
|
||||||
"stripe": "^20.3.1",
|
"stripe": "^20.3.1",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-dist": "^5.31.0"
|
"swagger-ui-dist": "^5.31.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -33,6 +34,50 @@
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||||
|
"version": "9.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
|
||||||
|
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jsdevtools/ono": "^7.1.3",
|
||||||
|
"@types/json-schema": "^7.0.6",
|
||||||
|
"call-me-maybe": "^1.0.1",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/openapi-schemas": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-methods": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-parser": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apidevtools/json-schema-ref-parser": "^9.0.6",
|
||||||
|
"@apidevtools/openapi-schemas": "^2.0.4",
|
||||||
|
"@apidevtools/swagger-methods": "^3.0.2",
|
||||||
|
"@jsdevtools/ono": "^7.1.3",
|
||||||
|
"call-me-maybe": "^1.0.1",
|
||||||
|
"z-schema": "^5.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"openapi-types": ">=7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||||
|
|
@ -548,6 +593,12 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jsdevtools/ono": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@pinojs/redact": {
|
"node_modules/@pinojs/redact": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||||
|
|
@ -1047,6 +1098,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/json-schema": {
|
||||||
|
"version": "7.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.11",
|
"version": "22.19.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
|
||||||
|
|
@ -1353,6 +1410,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/bare-events": {
|
"node_modules/bare-events": {
|
||||||
"version": "2.8.2",
|
"version": "2.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||||
|
|
@ -1477,6 +1540,16 @@
|
||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-crc32": {
|
"node_modules/buffer-crc32": {
|
||||||
"version": "0.2.13",
|
"version": "0.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
|
@ -1541,6 +1614,12 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-me-maybe": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
|
|
@ -1665,6 +1744,12 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
|
@ -1794,6 +1879,18 @@
|
||||||
"integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==",
|
"integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/doctrine": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"esutils": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
@ -2213,6 +2310,12 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs.realpath": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
|
@ -2348,6 +2451,27 @@
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/glob": {
|
||||||
|
"version": "7.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||||
|
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||||
|
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
|
@ -2513,6 +2637,17 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||||
|
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
|
@ -2582,6 +2717,26 @@
|
||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.get": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.mergewith": {
|
||||||
|
"version": "4.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||||
|
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/loupe": {
|
"node_modules/loupe": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
||||||
|
|
@ -2689,6 +2844,18 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mitt": {
|
"node_modules/mitt": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
|
|
@ -2794,6 +2961,13 @@
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openapi-types": {
|
||||||
|
"version": "12.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||||
|
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/pac-proxy-agent": {
|
"node_modules/pac-proxy-agent": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
||||||
|
|
@ -2888,6 +3062,15 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.12",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
|
|
@ -3810,6 +3993,56 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swagger-jsdoc": {
|
||||||
|
"version": "6.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
||||||
|
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "6.2.0",
|
||||||
|
"doctrine": "3.0.0",
|
||||||
|
"glob": "7.1.6",
|
||||||
|
"lodash.mergewith": "^4.6.2",
|
||||||
|
"swagger-parser": "^10.0.3",
|
||||||
|
"yaml": "2.0.0-1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"swagger-jsdoc": "bin/swagger-jsdoc.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-jsdoc/node_modules/commander": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-jsdoc/node_modules/yaml": {
|
||||||
|
"version": "2.0.0-1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
|
||||||
|
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-parser": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apidevtools/swagger-parser": "10.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/swagger-ui-dist": {
|
"node_modules/swagger-ui-dist": {
|
||||||
"version": "5.31.0",
|
"version": "5.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz",
|
||||||
|
|
@ -4036,6 +4269,15 @@
|
||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/validator": {
|
||||||
|
"version": "13.15.26",
|
||||||
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
|
||||||
|
"integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|
@ -4351,6 +4593,24 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/eemeli"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
|
@ -4388,6 +4648,36 @@
|
||||||
"fd-slicer": "~1.1.0"
|
"fd-slicer": "~1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/z-schema": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"validator": "^13.7.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"z-schema": "bin/z-schema"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"commander": "^9.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/z-schema/node_modules/commander": {
|
||||||
|
"version": "9.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||||
|
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:pages": "node scripts/build-pages.js && npx terser public/app.js -o public/app.min.js --compress --mangle",
|
"build:pages": "node scripts/build-pages.js && npx terser public/app.js -o public/app.min.js --compress --mangle",
|
||||||
"build": "npm run build:pages && tsc",
|
"build": "node scripts/generate-openapi.mjs && npm run build:pages && tsc",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "tsx src/index.ts",
|
"dev": "tsx src/index.ts",
|
||||||
"test": "vitest run"
|
"test": "vitest run",
|
||||||
|
"generate-openapi": "node scripts/generate-openapi.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
|
|
@ -22,6 +23,7 @@
|
||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"puppeteer": "^24.0.0",
|
"puppeteer": "^24.0.0",
|
||||||
"stripe": "^20.3.1",
|
"stripe": "^20.3.1",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-dist": "^5.31.0"
|
"swagger-ui-dist": "^5.31.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
1212
public/openapi.json
1212
public/openapi.json
File diff suppressed because it is too large
Load diff
123
scripts/generate-openapi.mjs
Normal file
123
scripts/generate-openapi.mjs
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Generates openapi.json from JSDoc annotations in route files.
|
||||||
|
* Run: node scripts/generate-openapi.mjs
|
||||||
|
* Output: public/openapi.json
|
||||||
|
*/
|
||||||
|
import swaggerJsdoc from 'swagger-jsdoc';
|
||||||
|
import { writeFileSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
definition: {
|
||||||
|
openapi: '3.0.3',
|
||||||
|
info: {
|
||||||
|
title: 'DocFast API',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: `Convert HTML, Markdown, and URLs to pixel-perfect PDFs. Built-in invoice & receipt templates.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
All conversion and template endpoints require an API key via \`Authorization: Bearer <key>\` or \`X-API-Key: <key>\` header.
|
||||||
|
|
||||||
|
## Demo Endpoints
|
||||||
|
Try the API without signing up! Demo endpoints are public (no API key needed) but rate-limited to 5 requests/hour per IP and produce watermarked PDFs.
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
- Demo: 5 PDFs/hour per IP (watermarked)
|
||||||
|
- Pro tier: 5,000 PDFs/month, 30 req/min
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
1. Try the demo at \`POST /v1/demo/html\` — no signup needed
|
||||||
|
2. Subscribe to Pro at [docfast.dev](https://docfast.dev/#pricing) for clean PDFs
|
||||||
|
3. Use your API key to convert documents`,
|
||||||
|
contact: {
|
||||||
|
name: 'DocFast',
|
||||||
|
url: 'https://docfast.dev',
|
||||||
|
email: 'support@docfast.dev'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{ url: 'https://docfast.dev', description: 'Production' }
|
||||||
|
],
|
||||||
|
tags: [
|
||||||
|
{ name: 'Demo', description: 'Try the API without signing up — watermarked PDFs, rate-limited' },
|
||||||
|
{ name: 'Conversion', description: 'Convert HTML, Markdown, or URLs to PDF (requires API key)' },
|
||||||
|
{ name: 'Templates', description: 'Built-in document templates' },
|
||||||
|
{ name: 'Account', description: 'Key recovery and email management' },
|
||||||
|
{ name: 'Billing', description: 'Stripe-powered subscription management' },
|
||||||
|
{ name: 'System', description: 'Health checks and usage stats' }
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
BearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
description: 'API key as Bearer token'
|
||||||
|
},
|
||||||
|
ApiKeyHeader: {
|
||||||
|
type: 'apiKey',
|
||||||
|
in: 'header',
|
||||||
|
name: 'X-API-Key',
|
||||||
|
description: 'API key via X-API-Key header'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
schemas: {
|
||||||
|
PdfOptions: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
format: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['A4', 'Letter', 'Legal', 'A3', 'A5', 'Tabloid'],
|
||||||
|
default: 'A4',
|
||||||
|
description: 'Page size'
|
||||||
|
},
|
||||||
|
landscape: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Landscape orientation'
|
||||||
|
},
|
||||||
|
margin: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
top: { type: 'string', description: 'Top margin (e.g. "10mm", "1in")', default: '0' },
|
||||||
|
right: { type: 'string', description: 'Right margin', default: '0' },
|
||||||
|
bottom: { type: 'string', description: 'Bottom margin', default: '0' },
|
||||||
|
left: { type: 'string', description: 'Left margin', default: '0' }
|
||||||
|
},
|
||||||
|
description: 'Page margins'
|
||||||
|
},
|
||||||
|
printBackground: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
description: 'Print background colors and images'
|
||||||
|
},
|
||||||
|
filename: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Custom filename for Content-Disposition header',
|
||||||
|
default: 'document.pdf'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Error: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
error: { type: 'string', description: 'Error message' }
|
||||||
|
},
|
||||||
|
required: ['error']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apis: [
|
||||||
|
join(__dirname, '../src/routes/*.ts'),
|
||||||
|
join(__dirname, '../src/openapi-extra.yaml')
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const spec = swaggerJsdoc(options);
|
||||||
|
const outPath = join(__dirname, '../public/openapi.json');
|
||||||
|
writeFileSync(outPath, JSON.stringify(spec, null, 2));
|
||||||
|
console.log(`✅ Generated ${outPath} (${Object.keys(spec.paths || {}).length} paths)`);
|
||||||
63
src/openapi-extra.yaml
Normal file
63
src/openapi-extra.yaml
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
paths:
|
||||||
|
/v1/signup/free:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Account
|
||||||
|
summary: Free signup (discontinued)
|
||||||
|
description: |
|
||||||
|
Free accounts have been discontinued. Use the demo endpoint for testing
|
||||||
|
or subscribe to Pro for production use.
|
||||||
|
deprecated: true
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
responses:
|
||||||
|
'410':
|
||||||
|
description: Free accounts discontinued
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
example: 'Free accounts have been discontinued.'
|
||||||
|
demo_endpoint:
|
||||||
|
type: string
|
||||||
|
example: '/v1/demo/html'
|
||||||
|
pro_url:
|
||||||
|
type: string
|
||||||
|
example: 'https://docfast.dev/#pricing'
|
||||||
|
/v1/usage:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- System
|
||||||
|
summary: Usage statistics (admin only)
|
||||||
|
description: Returns usage statistics for the authenticated user. Requires admin API key.
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- ApiKeyHeader: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Usage statistics
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
count:
|
||||||
|
type: integer
|
||||||
|
month:
|
||||||
|
type: string
|
||||||
|
'403':
|
||||||
|
description: Admin access required
|
||||||
|
'503':
|
||||||
|
description: Admin access not configured
|
||||||
|
|
@ -56,7 +56,35 @@ const checkoutLimiter = rateLimit({
|
||||||
message: { error: "Too many checkout requests. Please try again later." },
|
message: { error: "Too many checkout requests. Please try again later." },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a Stripe Checkout session for Pro subscription
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/billing/checkout:
|
||||||
|
* post:
|
||||||
|
* tags: [Billing]
|
||||||
|
* summary: Create a Stripe checkout session
|
||||||
|
* description: |
|
||||||
|
* Creates a Stripe Checkout session for a Pro subscription (€9/month).
|
||||||
|
* Returns a URL to redirect the user to Stripe's hosted payment page.
|
||||||
|
* Rate limited to 3 requests per hour per IP.
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Checkout session created
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* url:
|
||||||
|
* type: string
|
||||||
|
* format: uri
|
||||||
|
* description: Stripe Checkout URL to redirect the user to
|
||||||
|
* 413:
|
||||||
|
* description: Request body too large
|
||||||
|
* 429:
|
||||||
|
* description: Too many checkout requests
|
||||||
|
* 500:
|
||||||
|
* description: Failed to create checkout session
|
||||||
|
*/
|
||||||
router.post("/checkout", checkoutLimiter, async (req: Request, res: Response) => {
|
router.post("/checkout", checkoutLimiter, async (req: Request, res: Response) => {
|
||||||
// Reject suspiciously large request bodies (>1KB)
|
// Reject suspiciously large request bodies (>1KB)
|
||||||
const contentLength = parseInt(req.headers["content-length"] || "0", 10);
|
const contentLength = parseInt(req.headers["content-length"] || "0", 10);
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,55 @@ interface ConvertBody {
|
||||||
filename?: string;
|
filename?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /v1/convert/html
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/convert/html:
|
||||||
|
* post:
|
||||||
|
* tags: [Conversion]
|
||||||
|
* summary: Convert HTML to PDF
|
||||||
|
* description: Converts HTML content to a PDF document. Bare HTML fragments are automatically wrapped in a full HTML document.
|
||||||
|
* security:
|
||||||
|
* - BearerAuth: []
|
||||||
|
* - ApiKeyHeader: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* allOf:
|
||||||
|
* - type: object
|
||||||
|
* required: [html]
|
||||||
|
* properties:
|
||||||
|
* html:
|
||||||
|
* type: string
|
||||||
|
* description: HTML content to convert. Can be a full document or a fragment.
|
||||||
|
* example: '<h1>Hello World</h1><p>My first PDF</p>'
|
||||||
|
* css:
|
||||||
|
* type: string
|
||||||
|
* description: Optional CSS to inject (only used when html is a fragment, not a full document)
|
||||||
|
* example: 'body { font-family: sans-serif; padding: 40px; }'
|
||||||
|
* - $ref: '#/components/schemas/PdfOptions'
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: PDF document
|
||||||
|
* content:
|
||||||
|
* application/pdf:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
|
* 400:
|
||||||
|
* description: Missing html field
|
||||||
|
* 401:
|
||||||
|
* description: Missing API key
|
||||||
|
* 403:
|
||||||
|
* description: Invalid API key
|
||||||
|
* 415:
|
||||||
|
* description: Unsupported Content-Type (must be application/json)
|
||||||
|
* 429:
|
||||||
|
* description: Rate limit or usage limit exceeded
|
||||||
|
* 500:
|
||||||
|
* description: PDF generation failed
|
||||||
|
*/
|
||||||
convertRouter.post("/html", async (req: Request & { acquirePdfSlot?: () => Promise<void>; releasePdfSlot?: () => void }, res: Response) => {
|
convertRouter.post("/html", async (req: Request & { acquirePdfSlot?: () => Promise<void>; releasePdfSlot?: () => void }, res: Response) => {
|
||||||
let slotAcquired = false;
|
let slotAcquired = false;
|
||||||
try {
|
try {
|
||||||
|
|
@ -103,7 +151,54 @@ convertRouter.post("/html", async (req: Request & { acquirePdfSlot?: () => Promi
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /v1/convert/markdown
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/convert/markdown:
|
||||||
|
* post:
|
||||||
|
* tags: [Conversion]
|
||||||
|
* summary: Convert Markdown to PDF
|
||||||
|
* description: Converts Markdown content to HTML and then to a PDF document.
|
||||||
|
* security:
|
||||||
|
* - BearerAuth: []
|
||||||
|
* - ApiKeyHeader: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* allOf:
|
||||||
|
* - type: object
|
||||||
|
* required: [markdown]
|
||||||
|
* properties:
|
||||||
|
* markdown:
|
||||||
|
* type: string
|
||||||
|
* description: Markdown content to convert
|
||||||
|
* example: '# Hello World\n\nThis is **bold** and *italic*.'
|
||||||
|
* css:
|
||||||
|
* type: string
|
||||||
|
* description: Optional CSS to inject into the rendered HTML
|
||||||
|
* - $ref: '#/components/schemas/PdfOptions'
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: PDF document
|
||||||
|
* content:
|
||||||
|
* application/pdf:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
|
* 400:
|
||||||
|
* description: Missing markdown field
|
||||||
|
* 401:
|
||||||
|
* description: Missing API key
|
||||||
|
* 403:
|
||||||
|
* description: Invalid API key
|
||||||
|
* 415:
|
||||||
|
* description: Unsupported Content-Type
|
||||||
|
* 429:
|
||||||
|
* description: Rate limit or usage limit exceeded
|
||||||
|
* 500:
|
||||||
|
* description: PDF generation failed
|
||||||
|
*/
|
||||||
convertRouter.post("/markdown", async (req: Request & { acquirePdfSlot?: () => Promise<void>; releasePdfSlot?: () => void }, res: Response) => {
|
convertRouter.post("/markdown", async (req: Request & { acquirePdfSlot?: () => Promise<void>; releasePdfSlot?: () => void }, res: Response) => {
|
||||||
let slotAcquired = false;
|
let slotAcquired = false;
|
||||||
try {
|
try {
|
||||||
|
|
@ -153,7 +248,59 @@ convertRouter.post("/markdown", async (req: Request & { acquirePdfSlot?: () => P
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /v1/convert/url
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/convert/url:
|
||||||
|
* post:
|
||||||
|
* tags: [Conversion]
|
||||||
|
* summary: Convert URL to PDF
|
||||||
|
* description: |
|
||||||
|
* Fetches a URL and converts the rendered page to PDF. JavaScript is disabled for security.
|
||||||
|
* Private/internal IP addresses are blocked (SSRF protection). DNS is pinned to prevent rebinding.
|
||||||
|
* security:
|
||||||
|
* - BearerAuth: []
|
||||||
|
* - ApiKeyHeader: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* allOf:
|
||||||
|
* - type: object
|
||||||
|
* required: [url]
|
||||||
|
* properties:
|
||||||
|
* url:
|
||||||
|
* type: string
|
||||||
|
* format: uri
|
||||||
|
* description: URL to convert (http or https only)
|
||||||
|
* example: 'https://example.com'
|
||||||
|
* waitUntil:
|
||||||
|
* type: string
|
||||||
|
* enum: [load, domcontentloaded, networkidle0, networkidle2]
|
||||||
|
* default: domcontentloaded
|
||||||
|
* description: When to consider navigation finished
|
||||||
|
* - $ref: '#/components/schemas/PdfOptions'
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: PDF document
|
||||||
|
* content:
|
||||||
|
* application/pdf:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
|
* 400:
|
||||||
|
* description: Missing/invalid URL or URL resolves to private IP
|
||||||
|
* 401:
|
||||||
|
* description: Missing API key
|
||||||
|
* 403:
|
||||||
|
* description: Invalid API key
|
||||||
|
* 415:
|
||||||
|
* description: Unsupported Content-Type
|
||||||
|
* 429:
|
||||||
|
* description: Rate limit or usage limit exceeded
|
||||||
|
* 500:
|
||||||
|
* description: PDF generation failed
|
||||||
|
*/
|
||||||
convertRouter.post("/url", async (req: Request & { acquirePdfSlot?: () => Promise<void>; releasePdfSlot?: () => void }, res: Response) => {
|
convertRouter.post("/url", async (req: Request & { acquirePdfSlot?: () => Promise<void>; releasePdfSlot?: () => void }, res: Response) => {
|
||||||
let slotAcquired = false;
|
let slotAcquired = false;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,59 @@ function sanitizeFilename(name: string): string {
|
||||||
return name.replace(/[\x00-\x1f"\\\r\n]/g, "").trim() || "document.pdf";
|
return name.replace(/[\x00-\x1f"\\\r\n]/g, "").trim() || "document.pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /v1/demo/html
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/demo/html:
|
||||||
|
* post:
|
||||||
|
* tags: [Demo]
|
||||||
|
* summary: Convert HTML to PDF (demo)
|
||||||
|
* description: |
|
||||||
|
* Public endpoint — no API key required. Rate limited to 5 requests per hour per IP.
|
||||||
|
* Output PDFs include a DocFast watermark. Upgrade to Pro for clean output.
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* allOf:
|
||||||
|
* - type: object
|
||||||
|
* required: [html]
|
||||||
|
* properties:
|
||||||
|
* html:
|
||||||
|
* type: string
|
||||||
|
* description: HTML content to convert
|
||||||
|
* example: '<h1>Hello World</h1><p>My first PDF</p>'
|
||||||
|
* css:
|
||||||
|
* type: string
|
||||||
|
* description: Optional CSS to inject (used when html is a fragment)
|
||||||
|
* - $ref: '#/components/schemas/PdfOptions'
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Watermarked PDF document
|
||||||
|
* content:
|
||||||
|
* application/pdf:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
|
* 400:
|
||||||
|
* description: Missing html field
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 415:
|
||||||
|
* description: Unsupported Content-Type
|
||||||
|
* 429:
|
||||||
|
* description: Demo rate limit exceeded (5/hour)
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 503:
|
||||||
|
* description: Server busy
|
||||||
|
* 504:
|
||||||
|
* description: PDF generation timed out
|
||||||
|
*/
|
||||||
router.post("/html", async (req: Request & { acquirePdfSlot?: () => Promise<void>; releasePdfSlot?: () => void }, res: Response) => {
|
router.post("/html", async (req: Request & { acquirePdfSlot?: () => Promise<void>; releasePdfSlot?: () => void }, res: Response) => {
|
||||||
let slotAcquired = false;
|
let slotAcquired = false;
|
||||||
try {
|
try {
|
||||||
|
|
@ -93,7 +145,51 @@ router.post("/html", async (req: Request & { acquirePdfSlot?: () => Promise<void
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /v1/demo/markdown
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/demo/markdown:
|
||||||
|
* post:
|
||||||
|
* tags: [Demo]
|
||||||
|
* summary: Convert Markdown to PDF (demo)
|
||||||
|
* description: |
|
||||||
|
* Public endpoint — no API key required. Rate limited to 5 requests per hour per IP.
|
||||||
|
* Markdown is converted to HTML then rendered to PDF with a DocFast watermark.
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* allOf:
|
||||||
|
* - type: object
|
||||||
|
* required: [markdown]
|
||||||
|
* properties:
|
||||||
|
* markdown:
|
||||||
|
* type: string
|
||||||
|
* description: Markdown content to convert
|
||||||
|
* example: '# Hello World\n\nThis is **bold** and *italic*.'
|
||||||
|
* css:
|
||||||
|
* type: string
|
||||||
|
* description: Optional CSS to inject
|
||||||
|
* - $ref: '#/components/schemas/PdfOptions'
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Watermarked PDF document
|
||||||
|
* content:
|
||||||
|
* application/pdf:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
|
* 400:
|
||||||
|
* description: Missing markdown field
|
||||||
|
* 415:
|
||||||
|
* description: Unsupported Content-Type
|
||||||
|
* 429:
|
||||||
|
* description: Demo rate limit exceeded (5/hour)
|
||||||
|
* 503:
|
||||||
|
* description: Server busy
|
||||||
|
* 504:
|
||||||
|
* description: PDF generation timed out
|
||||||
|
*/
|
||||||
router.post("/markdown", async (req: Request & { acquirePdfSlot?: () => Promise<void>; releasePdfSlot?: () => void }, res: Response) => {
|
router.post("/markdown", async (req: Request & { acquirePdfSlot?: () => Promise<void>; releasePdfSlot?: () => void }, res: Response) => {
|
||||||
let slotAcquired = false;
|
let slotAcquired = false;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,56 @@ export const healthRouter = Router();
|
||||||
|
|
||||||
const HEALTH_CHECK_TIMEOUT_MS = 3000;
|
const HEALTH_CHECK_TIMEOUT_MS = 3000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /health:
|
||||||
|
* get:
|
||||||
|
* tags: [System]
|
||||||
|
* summary: Health check
|
||||||
|
* description: Returns service health status including database connectivity and browser pool stats.
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Service is healthy
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* enum: [ok, degraded]
|
||||||
|
* version:
|
||||||
|
* type: string
|
||||||
|
* example: '0.4.0'
|
||||||
|
* database:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* enum: [ok, error]
|
||||||
|
* version:
|
||||||
|
* type: string
|
||||||
|
* example: 'PostgreSQL 17.4'
|
||||||
|
* pool:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* size:
|
||||||
|
* type: integer
|
||||||
|
* active:
|
||||||
|
* type: integer
|
||||||
|
* available:
|
||||||
|
* type: integer
|
||||||
|
* queueDepth:
|
||||||
|
* type: integer
|
||||||
|
* pdfCount:
|
||||||
|
* type: integer
|
||||||
|
* restarting:
|
||||||
|
* type: boolean
|
||||||
|
* uptimeSeconds:
|
||||||
|
* type: integer
|
||||||
|
* 503:
|
||||||
|
* description: Service is degraded (database issue)
|
||||||
|
*/
|
||||||
healthRouter.get("/", async (_req, res) => {
|
healthRouter.get("/", async (_req, res) => {
|
||||||
const poolStats = getPoolStats();
|
const poolStats = getPoolStats();
|
||||||
let databaseStatus: any;
|
let databaseStatus: any;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,46 @@ const recoverLimiter = rateLimit({
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/recover:
|
||||||
|
* post:
|
||||||
|
* tags: [Account]
|
||||||
|
* summary: Request API key recovery
|
||||||
|
* description: |
|
||||||
|
* Sends a 6-digit verification code to the email address if an account exists.
|
||||||
|
* Response is always the same regardless of whether the email exists (to prevent enumeration).
|
||||||
|
* Rate limited to 3 requests per hour.
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required: [email]
|
||||||
|
* properties:
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* description: Email address associated with the API key
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Recovery code sent (or no-op if email not found)
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: recovery_sent
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* 400:
|
||||||
|
* description: Invalid email format
|
||||||
|
* 429:
|
||||||
|
* description: Too many recovery attempts
|
||||||
|
*/
|
||||||
router.post("/", recoverLimiter, async (req: Request, res: Response) => {
|
router.post("/", recoverLimiter, async (req: Request, res: Response) => {
|
||||||
const { email } = req.body || {};
|
const { email } = req.body || {};
|
||||||
|
|
||||||
|
|
@ -41,6 +81,52 @@ router.post("/", recoverLimiter, async (req: Request, res: Response) => {
|
||||||
res.json({ status: "recovery_sent", message: "If an account exists for this email, a verification code has been sent." });
|
res.json({ status: "recovery_sent", message: "If an account exists for this email, a verification code has been sent." });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/recover/verify:
|
||||||
|
* post:
|
||||||
|
* tags: [Account]
|
||||||
|
* summary: Verify recovery code and retrieve API key
|
||||||
|
* description: Verifies the 6-digit code sent via email and returns the API key if valid. Code expires after 15 minutes.
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required: [email, code]
|
||||||
|
* properties:
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* code:
|
||||||
|
* type: string
|
||||||
|
* pattern: '^\d{6}$'
|
||||||
|
* description: 6-digit verification code
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: API key recovered
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: recovered
|
||||||
|
* apiKey:
|
||||||
|
* type: string
|
||||||
|
* description: The recovered API key
|
||||||
|
* tier:
|
||||||
|
* type: string
|
||||||
|
* enum: [free, pro]
|
||||||
|
* 400:
|
||||||
|
* description: Invalid verification code or missing fields
|
||||||
|
* 410:
|
||||||
|
* description: Verification code expired
|
||||||
|
* 429:
|
||||||
|
* description: Too many failed attempts
|
||||||
|
*/
|
||||||
router.post("/verify", recoverLimiter, async (req: Request, res: Response) => {
|
router.post("/verify", recoverLimiter, async (req: Request, res: Response) => {
|
||||||
const { email, code } = req.body || {};
|
const { email, code } = req.body || {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,53 @@ function sanitizeFilename(name: string): string {
|
||||||
|
|
||||||
export const templatesRouter = Router();
|
export const templatesRouter = Router();
|
||||||
|
|
||||||
// GET /v1/templates — list available templates
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/templates:
|
||||||
|
* get:
|
||||||
|
* tags: [Templates]
|
||||||
|
* summary: List available templates
|
||||||
|
* description: Returns a list of all built-in document templates with their required fields.
|
||||||
|
* security:
|
||||||
|
* - BearerAuth: []
|
||||||
|
* - ApiKeyHeader: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: List of templates
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* templates:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: string
|
||||||
|
* example: invoice
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* example: Invoice
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* fields:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* required:
|
||||||
|
* type: boolean
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* 401:
|
||||||
|
* description: Missing API key
|
||||||
|
* 403:
|
||||||
|
* description: Invalid API key
|
||||||
|
*/
|
||||||
templatesRouter.get("/", (_req: Request, res: Response) => {
|
templatesRouter.get("/", (_req: Request, res: Response) => {
|
||||||
const list = Object.entries(templates).map(([id, t]) => ({
|
const list = Object.entries(templates).map(([id, t]) => ({
|
||||||
id,
|
id,
|
||||||
|
|
@ -20,7 +66,71 @@ templatesRouter.get("/", (_req: Request, res: Response) => {
|
||||||
res.json({ templates: list });
|
res.json({ templates: list });
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /v1/templates/:id/render — render template to PDF
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/templates/{id}/render:
|
||||||
|
* post:
|
||||||
|
* tags: [Templates]
|
||||||
|
* summary: Render a template to PDF
|
||||||
|
* description: |
|
||||||
|
* Renders a built-in template with the provided data and returns a PDF.
|
||||||
|
* Use GET /v1/templates to see available templates and their required fields.
|
||||||
|
* Special fields: `_format` (page size), `_margin` (page margins), `_filename` (output filename).
|
||||||
|
* security:
|
||||||
|
* - BearerAuth: []
|
||||||
|
* - ApiKeyHeader: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Template ID (e.g. "invoice", "receipt")
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* description: Template data (fields depend on template). Can also be passed at root level.
|
||||||
|
* _format:
|
||||||
|
* type: string
|
||||||
|
* enum: [A4, Letter, Legal, A3, A5, Tabloid]
|
||||||
|
* default: A4
|
||||||
|
* description: Page size override
|
||||||
|
* _margin:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* top: { type: string }
|
||||||
|
* right: { type: string }
|
||||||
|
* bottom: { type: string }
|
||||||
|
* left: { type: string }
|
||||||
|
* description: Page margin override
|
||||||
|
* _filename:
|
||||||
|
* type: string
|
||||||
|
* description: Custom output filename
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: PDF document
|
||||||
|
* content:
|
||||||
|
* application/pdf:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
|
* 400:
|
||||||
|
* description: Missing required template fields
|
||||||
|
* 401:
|
||||||
|
* description: Missing API key
|
||||||
|
* 403:
|
||||||
|
* description: Invalid API key
|
||||||
|
* 404:
|
||||||
|
* description: Template not found
|
||||||
|
* 500:
|
||||||
|
* description: Template rendering failed
|
||||||
|
*/
|
||||||
templatesRouter.post("/:id/render", async (req: Request, res: Response) => {
|
templatesRouter.post("/:id/render", async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const id = req.params.id as string;
|
const id = req.params.id as string;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue