diff --git a/.gitignore b/.gitignore
index fa4b2456e..a04e90d2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -276,3 +276,4 @@ website/.docusaurus
# Generated from testing
/test/fixtures/test-package/package-lock.json
+
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 000000000..5eedb4a47
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+src/service/generatedRoutes.ts
diff --git a/eslint.config.mjs b/eslint.config.mjs
index b074b622f..9f98185de 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -52,6 +52,7 @@ export default defineConfig(
// generated files we don't control
'**/package-lock.json',
'src/config/generated/**',
+ 'src/service/generatedRoutes.ts',
// has it's own eslint
'experimental/license-inventory',
// vendored code we're not changing
diff --git a/package-lock.json b/package-lock.json
index ed244b13f..556654db3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -53,6 +53,7 @@
"react-html-parser": "^2.0.2",
"react-router-dom": "6.30.3",
"simple-git": "^3.30.0",
+ "tsoa": "^7.0.0-alpha.0",
"uuid": "^13.0.0",
"validator": "^13.15.26",
"yargs": "^17.7.2"
@@ -2546,6 +2547,322 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@hapi/accept": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-6.0.3.tgz",
+ "integrity": "sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/ammo": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/ammo/-/ammo-6.0.1.tgz",
+ "integrity": "sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/b64": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz",
+ "integrity": "sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/boom": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz",
+ "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/bounce": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-3.0.2.tgz",
+ "integrity": "sha512-d0XmlTi3H9HFDHhQLjg4F4auL1EY3Wqj7j7/hGDhFFe6xAbnm3qiGrXeT93zZnPH8gH+SKAFYiRzu26xkXcH3g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/bourne": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz",
+ "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@hapi/call": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/call/-/call-9.0.1.tgz",
+ "integrity": "sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/catbox": {
+ "version": "12.1.1",
+ "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-12.1.1.tgz",
+ "integrity": "sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/podium": "^5.0.0",
+ "@hapi/validate": "^2.0.1"
+ }
+ },
+ "node_modules/@hapi/catbox-memory": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-6.0.2.tgz",
+ "integrity": "sha512-H1l4ugoFW/ZRkqeFrIo8p1rWN0PA4MDTfu4JmcoNDvnY975o29mqoZblqFTotxNHlEkMPpIiIBJTV+Mbi+aF0g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/content": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/content/-/content-6.0.1.tgz",
+ "integrity": "sha512-lQ2vOoFMNYxwKVnKf+3Pi3PfoviM4EJYlT9JbrBPfEc0xKMiVDqqXF8UTE1S1oKhHQliWSP5t6zTKNlmaXBGcQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.0"
+ }
+ },
+ "node_modules/@hapi/cryptiles": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-6.0.3.tgz",
+ "integrity": "sha512-r6VKalpbMHz4ci3gFjFysBmhwCg70RpYZy6OkjEpdXzAYnYFX5XsW7n4YMJvuIYpnMwLxGUjK/cBhA7X3JDvXw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@hapi/file": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@hapi/file/-/file-3.0.0.tgz",
+ "integrity": "sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@hapi/hapi": {
+ "version": "21.4.8",
+ "resolved": "https://registry.npmjs.org/@hapi/hapi/-/hapi-21.4.8.tgz",
+ "integrity": "sha512-l93IrEG4iQyM+yKdngWmkPtajkJGM81yfinmSFmiaNHG+r1fgsWaewwcE1hhsFnqPrVZpU8Y3PiVJMb6uT+01Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/accept": "^6.0.3",
+ "@hapi/ammo": "^6.0.1",
+ "@hapi/boom": "^10.0.1",
+ "@hapi/bounce": "^3.0.2",
+ "@hapi/call": "^9.0.1",
+ "@hapi/catbox": "^12.1.1",
+ "@hapi/catbox-memory": "^6.0.2",
+ "@hapi/heavy": "^8.0.1",
+ "@hapi/hoek": "^11.0.7",
+ "@hapi/mimos": "^7.0.1",
+ "@hapi/podium": "^5.0.2",
+ "@hapi/shot": "^6.0.2",
+ "@hapi/somever": "^4.1.1",
+ "@hapi/statehood": "^8.2.1",
+ "@hapi/subtext": "^8.1.2",
+ "@hapi/teamwork": "^6.0.1",
+ "@hapi/topo": "^6.0.2",
+ "@hapi/validate": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ }
+ },
+ "node_modules/@hapi/heavy": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-8.0.1.tgz",
+ "integrity": "sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/validate": "^2.0.1"
+ }
+ },
+ "node_modules/@hapi/hoek": {
+ "version": "11.0.7",
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz",
+ "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@hapi/iron": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/iron/-/iron-7.0.1.tgz",
+ "integrity": "sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/b64": "^6.0.1",
+ "@hapi/boom": "^10.0.1",
+ "@hapi/bourne": "^3.0.0",
+ "@hapi/cryptiles": "^6.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/mimos": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/mimos/-/mimos-7.0.1.tgz",
+ "integrity": "sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2",
+ "mime-db": "^1.52.0"
+ }
+ },
+ "node_modules/@hapi/nigel": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/nigel/-/nigel-5.0.1.tgz",
+ "integrity": "sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/vise": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@hapi/pez": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@hapi/pez/-/pez-6.1.1.tgz",
+ "integrity": "sha512-yg2OS1tC0S1sHXvhUtWsfRn6lrKl9jKtRhZ+EI0woOW/gqX5vM2PZ1459ypCvCYDRLJ9nIyueeEH5MJV1ZDqIg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/b64": "^6.0.1",
+ "@hapi/boom": "^10.0.1",
+ "@hapi/content": "^6.0.1",
+ "@hapi/hoek": "^11.0.7",
+ "@hapi/nigel": "^5.0.1"
+ }
+ },
+ "node_modules/@hapi/podium": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/podium/-/podium-5.0.2.tgz",
+ "integrity": "sha512-T7gf2JYHQQfEfewTQFbsaXoZxSvuXO/QBIGljucUQ/lmPnTTNAepoIKOakWNVWvo2fMEDjycu77r8k6dhreqHA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/teamwork": "^6.0.0",
+ "@hapi/validate": "^2.0.1"
+ }
+ },
+ "node_modules/@hapi/shot": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/shot/-/shot-6.0.2.tgz",
+ "integrity": "sha512-WKK1ShfJTrL1oXC0skoIZQYzvLsyMDEF8lfcWuQBjpjCN29qivr9U36ld1z0nt6edvzv28etNMOqUF4klnHryw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/validate": "^2.0.1"
+ }
+ },
+ "node_modules/@hapi/somever": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-4.1.1.tgz",
+ "integrity": "sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/bounce": "^3.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/statehood": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@hapi/statehood/-/statehood-8.2.1.tgz",
+ "integrity": "sha512-xf72TG/QINW26jUu+uL5H+crE1o8GplIgfPWwPZhnAGJzetIVAQEQYvzq+C0aEVHg5/lMMtQ+L9UryuSa5Yjkg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/bounce": "^3.0.1",
+ "@hapi/bourne": "^3.0.0",
+ "@hapi/cryptiles": "^6.0.1",
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/iron": "^7.0.1",
+ "@hapi/validate": "^2.0.1"
+ }
+ },
+ "node_modules/@hapi/subtext": {
+ "version": "8.1.2",
+ "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-8.1.2.tgz",
+ "integrity": "sha512-2x71YJHmFpCjhIhfiNZdKp63nh3xRPp7RrwH7JoO9R4Sd0DRzzRU/VfX2fMmUR7jcoS5qNET1WyGIaqKpMu/ng==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/bourne": "^3.0.0",
+ "@hapi/content": "^6.0.1",
+ "@hapi/file": "^3.0.0",
+ "@hapi/hoek": "^11.0.7",
+ "@hapi/pez": "^6.1.1",
+ "@hapi/wreck": "^18.1.0"
+ }
+ },
+ "node_modules/@hapi/teamwork": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-6.0.1.tgz",
+ "integrity": "sha512-52OXRslUfYwXAOG8k58f2h2ngXYQGP0x5RPOo+eWA/FtyLgHjGMrE3+e9LSXP/0q2YfHAK5wj9aA9DTy1K+kyQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@hapi/topo": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz",
+ "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/validate": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz",
+ "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/topo": "^6.0.1"
+ }
+ },
+ "node_modules/@hapi/vise": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-5.0.1.tgz",
+ "integrity": "sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/wreck": {
+ "version": "18.1.0",
+ "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.1.0.tgz",
+ "integrity": "sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/bourne": "^3.0.0",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"dev": true,
@@ -4183,6 +4500,339 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@tsoa/cli": {
+ "version": "7.0.0-alpha.0",
+ "resolved": "https://registry.npmjs.org/@tsoa/cli/-/cli-7.0.0-alpha.0.tgz",
+ "integrity": "sha512-fCBWv6F20qrpwFh5X+vaZs38Bh/5cVwKPhvG/uArR+crZgEowyNl76unLCmYdvycES9Y6g/TKFLagG8qojSNuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tsoa/runtime": "^7.0.0-alpha.0",
+ "@types/multer": "^1.4.12",
+ "fs-extra": "^11.2.0",
+ "glob": "^10.3.10",
+ "handlebars": "^4.7.8",
+ "merge-anything": "^5.1.7",
+ "minimatch": "^9.0.1",
+ "ts-deepmerge": "^7.0.2",
+ "typescript": "^5.7.2",
+ "validator": "^13.12.0",
+ "yaml": "^2.6.1",
+ "yargs": "^17.7.1"
+ },
+ "bin": {
+ "tsoa": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "yarn": ">=1.9.4"
+ }
+ },
+ "node_modules/@tsoa/cli/node_modules/brace-expansion": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@tsoa/cli/node_modules/fs-extra": {
+ "version": "11.3.4",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
+ "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/@tsoa/cli/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@tsoa/runtime": {
+ "version": "7.0.0-alpha.0",
+ "resolved": "https://registry.npmjs.org/@tsoa/runtime/-/runtime-7.0.0-alpha.0.tgz",
+ "integrity": "sha512-zlWYz2bLfaN6WtFoIbLBEAyVhKG4IQKJ9QPzeRFFKeAsh5zYN4/ocnd14XyWs4ehY9TdtTnma2drW89aYNSRYw==",
+ "license": "MIT",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hapi": "^21.3.12",
+ "@types/koa": "^2.15.0",
+ "@types/multer": "^1.4.12",
+ "express": "^4.21.2",
+ "reflect-metadata": "^0.2.2",
+ "validator": "^13.12.0"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "yarn": ">=1.9.4"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/@tsoa/runtime/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/@tsoa/runtime/node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/@tsoa/runtime/node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@types/accepts": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz",
+ "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/activedirectory2": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@types/activedirectory2/-/activedirectory2-1.2.6.tgz",
@@ -4232,7 +4882,6 @@
},
"node_modules/@types/body-parser": {
"version": "1.19.5",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
@@ -4241,12 +4890,17 @@
},
"node_modules/@types/connect": {
"version": "3.4.38",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
+ "node_modules/@types/content-disposition": {
+ "version": "0.5.9",
+ "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz",
+ "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==",
+ "license": "MIT"
+ },
"node_modules/@types/conventional-commits-parser": {
"version": "5.0.1",
"dev": true,
@@ -4260,6 +4914,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/cookies": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.2.tgz",
+ "integrity": "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/express": "*",
+ "@types/keygrip": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
@@ -4302,7 +4968,6 @@
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
@@ -4322,7 +4987,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
"integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -4352,9 +5016,14 @@
"domhandler": "^2.4.0"
}
},
+ "node_modules/@types/http-assert": {
+ "version": "1.5.6",
+ "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz",
+ "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==",
+ "license": "MIT"
+ },
"node_modules/@types/http-errors": {
"version": "2.0.4",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
@@ -4380,6 +5049,37 @@
"@types/node": "*"
}
},
+ "node_modules/@types/keygrip": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz",
+ "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/koa": {
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz",
+ "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/accepts": "*",
+ "@types/content-disposition": "*",
+ "@types/cookies": "*",
+ "@types/http-assert": "*",
+ "@types/http-errors": "*",
+ "@types/keygrip": "*",
+ "@types/koa-compose": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/koa-compose": {
+ "version": "3.2.9",
+ "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.9.tgz",
+ "integrity": "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/koa": "*"
+ }
+ },
"node_modules/@types/ldapjs": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-3.0.6.tgz",
@@ -4416,7 +5116,6 @@
},
"node_modules/@types/mime": {
"version": "1.3.5",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
@@ -4426,6 +5125,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/multer": {
+ "version": "1.4.13",
+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",
+ "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "22.19.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.8.tgz",
@@ -4474,12 +5182,10 @@
},
"node_modules/@types/qs": {
"version": "6.9.18",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
@@ -4525,7 +5231,6 @@
},
"node_modules/@types/send": {
"version": "0.17.4",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
@@ -4536,7 +5241,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
@@ -5445,6 +6149,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
"node_modules/array-ify": {
"version": "1.0.0",
"dev": true,
@@ -5795,26 +6505,6 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/body-parser/node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
- "license": "MIT",
- "dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
@@ -5846,15 +6536,6 @@
"node": ">= 0.10"
}
},
- "node_modules/body-parser/node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/bowser": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
@@ -7044,6 +7725,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
"node_modules/dezalgo": {
"version": "1.0.4",
"dev": true,
@@ -8748,7 +9439,6 @@
},
"node_modules/graceful-fs": {
"version": "4.2.11",
- "dev": true,
"license": "ISC"
},
"node_modules/graphql": {
@@ -8759,6 +9449,27 @@
"iterall": "1.1.3"
}
},
+ "node_modules/handlebars": {
+ "version": "4.7.9",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
+ "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.2",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
"node_modules/has-bigints": {
"version": "1.1.0",
"dev": true,
@@ -8903,17 +9614,23 @@
}
},
"node_modules/http-errors": {
- "version": "2.0.0",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
- "depd": "2.0.0",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "toidentifier": "1.0.1"
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/http-signature": {
@@ -9548,6 +10265,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-what": {
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
+ "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
"node_modules/is-windows": {
"version": "1.0.2",
"dev": true,
@@ -9941,7 +10670,6 @@
},
"node_modules/jsonfile": {
"version": "6.1.0",
- "dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
@@ -10898,6 +11626,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/merge-anything": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz",
+ "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
@@ -10934,7 +11677,6 @@
},
"node_modules/methods": {
"version": "1.1.2",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -11155,6 +11897,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "license": "MIT"
+ },
"node_modules/node-fetch": {
"version": "2.7.0",
"dev": true,
@@ -12367,13 +13115,15 @@
}
},
"node_modules/raw-body": {
- "version": "2.5.2",
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
@@ -12504,6 +13254,12 @@
"node": ">= 6"
}
},
+ "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"
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"dev": true,
@@ -13170,7 +13926,6 @@
},
"node_modules/source-map": {
"version": "0.6.1",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -13267,7 +14022,9 @@
"license": "MIT"
},
"node_modules/statuses": {
- "version": "2.0.1",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -13912,6 +14669,15 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/ts-deepmerge": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.3.tgz",
+ "integrity": "sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14.13.1"
+ }
+ },
"node_modules/ts-node": {
"version": "10.9.2",
"dev": true,
@@ -13988,6 +14754,23 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
+ "node_modules/tsoa": {
+ "version": "7.0.0-alpha.0",
+ "resolved": "https://registry.npmjs.org/tsoa/-/tsoa-7.0.0-alpha.0.tgz",
+ "integrity": "sha512-o5h2DD1IKa2GF728BHYDL2uSX57a44sX8BLFScR4KbD0xlOmgsSDECneJmouDxf9MJdjHHWc+I1S3sGK82B6kQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tsoa/cli": "^7.0.0-alpha.0",
+ "@tsoa/runtime": "^7.0.0-alpha.0"
+ },
+ "bin": {
+ "tsoa": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "yarn": ">=1.9.4"
+ }
+ },
"node_modules/tsscmp": {
"version": "1.0.6",
"license": "MIT",
@@ -14160,7 +14943,6 @@
},
"node_modules/typescript": {
"version": "5.9.3",
- "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -14202,6 +14984,19 @@
"node": ">=8"
}
},
+ "node_modules/uglify-js": {
+ "version": "3.19.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/uid-safe": {
"version": "2.1.5",
"license": "MIT",
@@ -14269,7 +15064,6 @@
},
"node_modules/universalify": {
"version": "2.0.1",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
@@ -14917,7 +15711,6 @@
},
"node_modules/wordwrap": {
"version": "1.0.0",
- "dev": true,
"license": "MIT"
},
"node_modules/wordwrapjs": {
@@ -15068,7 +15861,6 @@
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
- "dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
diff --git a/package.json b/package.json
index 782d645df..e8dd85ee9 100644
--- a/package.json
+++ b/package.json
@@ -54,10 +54,11 @@
"server": "cross-env ALLOWED_ORIGINS=* tsx index.ts",
"start": "concurrently \"npm run server\" \"npm run client\"",
"build": "npm run generate-config-types && npm run build-ui && npm run build-ts",
- "build-ts": "tsc --project tsconfig.publish.json && node scripts/fix-shebang.js",
+ "build-ts": "npm run build-tsoa && tsc --project tsconfig.publish.json && node scripts/fix-shebang.js",
"build-ui": "vite build",
+ "build-tsoa": "tsoa spec-and-routes",
"check-types": "tsc",
- "check-types:server": "tsc --project tsconfig.publish.json --noEmit",
+ "check-types:server": "npm run build-tsoa && tsc --project tsconfig.publish.json --noEmit",
"test-shuffle": "NODE_ENV=test vitest --run --dir ./test --sequence.shuffle",
"test": "cross-env NODE_ENV=test vitest --run --dir ./test",
"test:e2e": "vitest run --config vitest.config.e2e.ts",
@@ -69,9 +70,10 @@
"prepare": "node ./scripts/prepare.js",
"lint": "eslint",
"lint:fix": "eslint --fix",
- "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css,scss}\" --ignore-path .gitignore --config ./.prettierrc",
- "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css,scss}\" --ignore-path .gitignore --config ./.prettierrc",
+ "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css,scss}\" --config ./.prettierrc",
+ "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css,scss}\" --config ./.prettierrc",
"gen-schema-doc": "node ./scripts/doc-schema.js",
+ "gen-swagger-doc": "node ./scripts/doc-swagger.js",
"cypress:run": "cypress run",
"cypress:run:docker": "cypress run --config specPattern='cypress/e2e/docker/**/*.cy.{js,ts}'",
"cypress:open": "cypress open",
@@ -137,6 +139,7 @@
"react-html-parser": "^2.0.2",
"react-router-dom": "6.30.3",
"simple-git": "^3.30.0",
+ "tsoa": "^7.0.0-alpha.0",
"uuid": "^13.0.0",
"validator": "^13.15.26",
"yargs": "^17.7.2"
@@ -222,6 +225,9 @@
],
"test/**/*.{js,jsx,ts,tsx,json}": [
"eslint --fix"
+ ],
+ "{src/service/controllers/**/*Controller.ts,src/service/authentication.ts,src/service/generatedRoutes.ts,src/app.ts,tsoa.json}": [
+ "node scripts/check-tsoa-routes.js --"
]
}
}
diff --git a/scripts/check-tsoa-routes.js b/scripts/check-tsoa-routes.js
new file mode 100644
index 000000000..277b1cb18
--- /dev/null
+++ b/scripts/check-tsoa-routes.js
@@ -0,0 +1,39 @@
+#!/usr/bin/env node
+
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Invoked by lint-staged when any TSOA input is staged. Regenerates
+ * src/service/generatedRoutes.ts via `npm run build-tsoa` and stages it
+ * so the commit always includes routes in sync with their inputs —
+ * the same fix-then-continue behaviour as `prettier --write`.
+ */
+const { spawnSync } = require('node:child_process');
+
+const build = spawnSync('npm run --silent build-tsoa', {
+ stdio: 'inherit',
+ shell: true,
+});
+if (build.status !== 0) {
+ process.exit(build.status ?? 1);
+}
+
+const add = spawnSync('git add src/service/generatedRoutes.ts', {
+ stdio: 'inherit',
+ shell: true,
+});
+process.exit(add.status ?? 0);
diff --git a/scripts/doc-swagger.js b/scripts/doc-swagger.js
new file mode 100644
index 000000000..c5f08259e
--- /dev/null
+++ b/scripts/doc-swagger.js
@@ -0,0 +1,362 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const { readFileSync, writeFileSync, mkdirSync } = require('fs');
+const { join } = require('path');
+
+const SWAGGER_FILE = './dist/swagger.json';
+const OUTPUT_DIR = './website/docs/api';
+
+const METHOD_COLORS = {
+ get: '#61affe',
+ post: '#49cc90',
+ put: '#fca130',
+ patch: '#50e3c2',
+ delete: '#f93e3e',
+};
+
+const STATUS_DESCRIPTIONS = {
+ 200: 'OK',
+ 201: 'Created',
+ 204: 'No Content',
+ 400: 'Bad Request',
+ 401: 'Unauthorized',
+ 403: 'Forbidden',
+ 404: 'Not Found',
+ 409: 'Conflict',
+ 500: 'Internal Server Error',
+};
+
+function loadSpec() {
+ const raw = readFileSync(SWAGGER_FILE, 'utf-8');
+ return JSON.parse(raw);
+}
+
+function httpMethodOrder(method) {
+ const order = { get: 0, post: 1, put: 2, patch: 3, delete: 4 };
+ return order[method] ?? 5;
+}
+
+function resolveRef(ref, spec) {
+ const path = ref.replace('#/', '').split('/');
+ let current = spec;
+ for (const segment of path) {
+ current = current[segment];
+ if (!current) return null;
+ }
+ return current;
+}
+
+function resolveSchema(schema, spec) {
+ if (!schema) return null;
+ if (schema.$ref) return resolveRef(schema.$ref, spec);
+ return schema;
+}
+
+function formatType(schema, spec) {
+ if (!schema) return '`any`';
+ const resolved = resolveSchema(schema, spec);
+ if (!resolved) return '`any`';
+
+ if (resolved.type === 'array') {
+ const itemType = resolved.items ? formatType(resolved.items, spec) : '`any`';
+ return `${itemType}[]`;
+ }
+
+ if (schema.$ref) {
+ const name = schema.$ref.split('/').pop();
+ return `\`${name}\``;
+ }
+
+ if (resolved.enum) {
+ return resolved.enum.map((v) => `\`"${v}"\``).join(' \\| ');
+ }
+
+ return `\`${resolved.type || 'object'}\``;
+}
+
+function methodBadge(method) {
+ const color = METHOD_COLORS[method] || '#999';
+ const label = method.toUpperCase();
+ return `${label}`;
+}
+
+function authBadge() {
+ return `AUTH`;
+}
+
+function statusBadge(code) {
+ let color = '#49cc90';
+ if (code >= 400 && code < 500) color = '#fca130';
+ if (code >= 500) color = '#f93e3e';
+ if (code >= 300 && code < 400) color = '#61affe';
+ return `${code}`;
+}
+
+function renderSchemaProperties(schema, spec) {
+ const resolved = resolveSchema(schema, spec);
+ if (!resolved || !resolved.properties) return '';
+
+ const required = new Set(resolved.required || []);
+ let lines = [];
+
+ lines.push('| Field | Type | Required | Description |');
+ lines.push('|-------|------|:--------:|-------------|');
+
+ for (const [name, prop] of Object.entries(resolved.properties)) {
+ const propResolved = resolveSchema(prop, spec);
+ const type = formatType(prop, spec);
+ const isRequired = required.has(name) ? '**Yes**' : 'No';
+ const desc = (propResolved?.description || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
+ lines.push(`| \`${name}\` | ${type} | ${isRequired} | ${desc} |`);
+ }
+
+ return lines.join('\n');
+}
+
+function renderParameters(parameters, spec) {
+ if (!parameters || parameters.length === 0) return '';
+
+ const params = parameters.map((p) => (p.$ref ? resolveRef(p.$ref, spec) : p)).filter(Boolean);
+ if (params.length === 0) return '';
+
+ let lines = [];
+ lines.push('#### Parameters\n');
+ lines.push('| Name | In | Type | Required | Description |');
+ lines.push('|------|:---:|------|:--------:|-------------|');
+
+ for (const param of params) {
+ const type = formatType(param.schema, spec);
+ const required = param.required ? '**Yes**' : 'No';
+ const desc = (param.description || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
+ const inBadge = param.in === 'path' ? '`path`' : '`query`';
+ lines.push(`| \`${param.name}\` | ${inBadge} | ${type} | ${required} | ${desc} |`);
+ }
+
+ return lines.join('\n');
+}
+
+function renderRequestBody(requestBody, spec) {
+ if (!requestBody) return '';
+
+ const content = requestBody.content;
+ if (!content) return '';
+
+ const jsonContent = content['application/json'];
+ if (!jsonContent || !jsonContent.schema) return '';
+
+ const schema = resolveSchema(jsonContent.schema, spec);
+ if (!schema) return '';
+
+ let lines = ['#### Request Body\n'];
+ const table = renderSchemaProperties(jsonContent.schema, spec);
+ if (table) {
+ lines.push(table);
+ } else {
+ lines.push(`Type: ${formatType(jsonContent.schema, spec)}`);
+ }
+ return lines.join('\n');
+}
+
+function renderResponses(responses) {
+ if (!responses) return '';
+
+ let lines = ['#### Responses\n'];
+
+ for (const [status, response] of Object.entries(responses)) {
+ const code = parseInt(status, 10);
+ const badge = statusBadge(code);
+ const desc = response.description || STATUS_DESCRIPTIONS[code] || '';
+ lines.push(`- ${badge} ${desc}`);
+ }
+
+ return lines.join('\n');
+}
+
+function renderSecurityNote(security) {
+ if (!security || security.length === 0) return '';
+ return ':::info[Authorization Required]\nThis endpoint requires a valid **JWT Bearer token** in the `Authorization` header.\n:::';
+}
+
+function countEndpoints(operations) {
+ const methods = {};
+ for (const op of operations) {
+ const m = op.method.toUpperCase();
+ methods[m] = (methods[m] || 0) + 1;
+ }
+ return Object.entries(methods)
+ .map(([m, c]) => `${c} ${m}`)
+ .join(', ');
+}
+
+function generateTagPage(tag, operations, spec) {
+ let lines = [];
+
+ lines.push('---');
+ lines.push(`title: ${tag}`);
+ lines.push(`description: ${tag} API endpoints for GitProxy`);
+ lines.push('---');
+ lines.push('');
+ lines.push(`# ${tag}\n`);
+
+ const tagDesc = (spec.tags || []).find((t) => t.name === tag);
+ if (tagDesc && tagDesc.description) {
+ lines.push(`${tagDesc.description}\n`);
+ }
+
+ lines.push(
+ `*${operations.length} endpoint${operations.length !== 1 ? 's' : ''} — ${countEndpoints(operations)}*\n`,
+ );
+
+ operations.sort((a, b) => {
+ const methodDiff = httpMethodOrder(a.method) - httpMethodOrder(b.method);
+ if (methodDiff !== 0) return methodDiff;
+ return a.path.localeCompare(b.path);
+ });
+
+ for (const op of operations) {
+ const badge = methodBadge(op.method);
+ const auth = op.security && op.security.length > 0 ? authBadge() : '';
+
+ lines.push(`## ${badge} \`${op.path}\`${auth}\n`);
+
+ if (op.description) {
+ lines.push(`${op.description}\n`);
+ } else if (op.summary) {
+ lines.push(`${op.summary}\n`);
+ }
+
+ const secNote = renderSecurityNote(op.security);
+ if (secNote) {
+ lines.push(`${secNote}\n`);
+ }
+
+ const params = renderParameters(op.parameters, spec);
+ if (params) {
+ lines.push(`${params}\n`);
+ }
+
+ const body = renderRequestBody(op.requestBody, spec);
+ if (body) {
+ lines.push(`${body}\n`);
+ }
+
+ const responses = renderResponses(op.responses);
+ if (responses) {
+ lines.push(`${responses}\n`);
+ }
+
+ lines.push('---\n');
+ }
+
+ return lines.join('\n');
+}
+
+function generateIndexPage(spec, tagList, tagOperations) {
+ const info = spec.info || {};
+ let lines = [];
+
+ lines.push('---');
+ lines.push('title: API Reference');
+ lines.push('description: REST API reference documentation for GitProxy');
+ lines.push('---');
+ lines.push('');
+ lines.push('# API Reference\n');
+
+ if (info.description) {
+ lines.push(`${info.description}\n`);
+ }
+
+ lines.push(`:::info[API Version]\n**${info.version || 'unknown'}** — OpenAPI 3.0\n:::\n`);
+
+ const securitySchemes = spec.components?.securitySchemes;
+ if (securitySchemes) {
+ lines.push('## Authentication\n');
+ for (const [name, scheme] of Object.entries(securitySchemes)) {
+ if (scheme.type === 'http') {
+ lines.push(
+ `Most endpoints require a **${scheme.scheme.toUpperCase()}** token${scheme.bearerFormat ? ` (${scheme.bearerFormat})` : ''} passed via the \`Authorization\` header:\n`,
+ );
+ lines.push('```\nAuthorization: Bearer \n```\n');
+ } else {
+ lines.push(`- **${name}**: ${scheme.type}\n`);
+ }
+ }
+ }
+
+ lines.push('## Endpoints\n');
+
+ const totalEndpoints = Object.values(tagOperations).reduce((sum, ops) => sum + ops.length, 0);
+ lines.push(`${totalEndpoints} endpoints across ${tagList.length} groups:\n`);
+
+ for (const tag of tagList) {
+ const slug = tag.toLowerCase().replace(/\s+/g, '-');
+ const ops = tagOperations[tag];
+ const summary = countEndpoints(ops);
+ lines.push(`- [**${tag}**](/docs/api/${slug}) — ${summary}`);
+ }
+
+ lines.push('');
+ return lines.join('\n');
+}
+
+function main() {
+ let spec;
+ try {
+ spec = loadSpec();
+ } catch (err) {
+ console.error(`Failed to read ${SWAGGER_FILE}: ${err.message}`);
+ console.error('Run "npm run build-tsoa" first to generate the OpenAPI spec.');
+ process.exit(1);
+ }
+
+ const tagOperations = {};
+
+ for (const [path, methods] of Object.entries(spec.paths || {})) {
+ for (const [method, operation] of Object.entries(methods)) {
+ if (['parameters', 'summary', 'description'].includes(method)) continue;
+
+ const tags = operation.tags || ['Default'];
+ for (const tag of tags) {
+ if (!tagOperations[tag]) tagOperations[tag] = [];
+ tagOperations[tag].push({
+ path,
+ method,
+ ...operation,
+ });
+ }
+ }
+ }
+
+ mkdirSync(OUTPUT_DIR, { recursive: true });
+
+ const tagList = Object.keys(tagOperations).sort();
+
+ const indexContent = generateIndexPage(spec, tagList, tagOperations);
+ writeFileSync(join(OUTPUT_DIR, 'index.mdx'), indexContent);
+ console.log(`Wrote ${join(OUTPUT_DIR, 'index.mdx')}`);
+
+ for (const [tag, operations] of Object.entries(tagOperations)) {
+ const slug = tag.toLowerCase().replace(/\s+/g, '-');
+ const content = generateTagPage(tag, operations, spec);
+ writeFileSync(join(OUTPUT_DIR, `${slug}.mdx`), content);
+ console.log(`Wrote ${join(OUTPUT_DIR, `${slug}.mdx`)}`);
+ }
+
+ console.log(`\nGenerated API docs for ${tagList.length} tag(s): ${tagList.join(', ')}`);
+}
+
+main();
diff --git a/src/service/routes/healthcheck.ts b/src/app.ts
similarity index 74%
rename from src/service/routes/healthcheck.ts
rename to src/app.ts
index bdc79aa2e..6ad0f79c4 100644
--- a/src/service/routes/healthcheck.ts
+++ b/src/app.ts
@@ -14,14 +14,8 @@
* limitations under the License.
*/
-import express, { Request, Response } from 'express';
+import express from 'express';
-const router = express.Router();
-
-router.get('/', (_req: Request, res: Response) => {
- res.send({
- message: 'ok',
- });
-});
-
-export default router;
+// Minimal entry point used by tsoa for controller/spec discovery.
+// The actual configured Express app lives in src/service/index.ts.
+export const app = express();
diff --git a/src/service/authentication.ts b/src/service/authentication.ts
new file mode 100644
index 000000000..c0b4e4489
--- /dev/null
+++ b/src/service/authentication.ts
@@ -0,0 +1,93 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Request } from 'express';
+import { getAPIAuthMethods } from '../config';
+import { assignRoles, validateJwt } from './passport/jwtUtils';
+import type { RoleMapping } from '../config/generated/config';
+
+/**
+ * tsoa authentication handler called for every route decorated with @Security.
+ *
+ * Supported security names:
+ * - 'jwt': Bearer-token validation via OIDC JWT or an existing session.
+ */
+export async function expressAuthentication(
+ request: Request,
+ securityName: string,
+ _scopes?: string[],
+): Promise {
+ if (securityName === 'jwt') {
+ // Already authenticated via session (e.g. passport local login)
+ if (request.isAuthenticated && request.isAuthenticated()) {
+ return request.user;
+ }
+
+ const apiAuthMethods = getAPIAuthMethods();
+ const jwtAuthMethod = apiAuthMethods.find((m) => m.type.toLowerCase() === 'jwt');
+
+ if (!jwtAuthMethod || !jwtAuthMethod.enabled) {
+ // JWT not configured — pass through (other middleware may enforce auth)
+ return;
+ }
+
+ const token = request.header('Authorization');
+ if (!token) {
+ throw Object.assign(new Error('No token provided'), { status: 401 });
+ }
+
+ if (!jwtAuthMethod.jwtConfig) {
+ console.log('JWT configuration is missing');
+ throw Object.assign(new Error('JWT configuration is missing'), { status: 500 });
+ }
+
+ const { clientID, authorityURL, expectedAudience, roleMapping } = jwtAuthMethod.jwtConfig;
+
+ if (!authorityURL) {
+ console.log('OIDC authority URL is not configured');
+ throw Object.assign(new Error('OIDC authority URL is not configured'), { status: 500 });
+ }
+
+ if (!clientID) {
+ console.log('OIDC client ID is not configured');
+ throw Object.assign(new Error('OIDC client ID is not configured'), { status: 500 });
+ }
+
+ const audience = expectedAudience || clientID;
+ const tokenParts = token.split(' ');
+ const accessToken = tokenParts.length === 2 ? tokenParts[1] : tokenParts[0];
+
+ const { verifiedPayload, error } = await validateJwt(
+ accessToken,
+ authorityURL,
+ audience,
+ clientID,
+ );
+
+ if (error || !verifiedPayload) {
+ console.log('JWT validation failed');
+ throw Object.assign(new Error(error || 'JWT validation failed'), { status: 401 });
+ }
+
+ request.user = verifiedPayload;
+ assignRoles(roleMapping as RoleMapping, verifiedPayload, request.user);
+
+ console.log('JWT validation successful');
+ return verifiedPayload;
+ }
+
+ throw Object.assign(new Error(`Unknown security scheme: ${securityName}`), { status: 401 });
+}
diff --git a/src/service/controllers/AuthController.ts b/src/service/controllers/AuthController.ts
new file mode 100644
index 000000000..ed8b885cf
--- /dev/null
+++ b/src/service/controllers/AuthController.ts
@@ -0,0 +1,310 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Request as ExpressRequest, Response as ExpressResponse, NextFunction } from 'express';
+import { Body, Controller, Get, Middlewares, Post, Request, Res, Route, Tags } from 'tsoa';
+import { getPassport, authStrategies } from '../passport';
+import { getAuthMethods } from '../../config';
+import * as db from '../../db';
+import * as passportLocal from '../passport/local';
+import * as passportAD from '../passport/activeDirectory';
+import { User } from '../../db/types';
+import { AuthenticationElement } from '../../config/generated/config';
+import { isAdminUser, toPublicUser } from '../routes/utils';
+import { handleErrorAndLog } from '../../utils/errors';
+import { PublicUser } from '../../db/types';
+import {
+ AuthResources,
+ AuthConfigResponse,
+ LoginResponse,
+ LogoutResponse,
+ CreateUserResponse,
+ GitAccountBody,
+ CreateUserBody,
+} from '../interfaces/auth.interfaces';
+import {
+ ForbiddenResponse,
+ InternalServerErrorResponse,
+ NotFoundResponse,
+ UnauthorisedResponse,
+ ValidationErrorResponse,
+} from '../decorators/response.types';
+
+const { GIT_PROXY_UI_HOST: uiHost = 'http://localhost', GIT_PROXY_UI_PORT: uiPort = 3000 } =
+ process.env;
+
+// login strategies that will work with /login e.g. take username and password
+const appropriateLoginStrategies = [passportLocal.type, passportAD.type];
+
+// getLoginStrategy fetches the enabled auth methods and identifies if there's an appropriate
+// auth method for username and password login. If there isn't it returns null, if there is it
+// returns the first.
+const getLoginStrategy = () => {
+ // returns only enabled auth methods
+ // returns at least one enabled auth method
+ const enabledAppropriateLoginStrategies = getAuthMethods().filter((am: AuthenticationElement) =>
+ appropriateLoginStrategies.includes(am.type.toLowerCase()),
+ );
+ // for where no login strategies which work for /login are enabled
+ // just return null
+ if (enabledAppropriateLoginStrategies.length === 0) {
+ return null;
+ }
+ // return the first enabled auth method
+ return enabledAppropriateLoginStrategies[0].type.toLowerCase();
+};
+
+/**
+ * Dynamically selects the login passport strategy and runs it as Express middleware.
+ * Used by `@Middlewares` on the POST /login route.
+ */
+export function dynamicLoginMiddleware(
+ req: ExpressRequest,
+ res: ExpressResponse,
+ next: NextFunction,
+): void {
+ const authType = getLoginStrategy();
+ if (authType === null) {
+ res.status(403).send('Username and Password based Login is not enabled at this time').end();
+ return;
+ }
+ getPassport().authenticate(authType)(req, res, next);
+}
+
+/**
+ * Handles the OIDC callback: authenticates the user and redirects on success.
+ * Used by @Middlewares on the GET /openidconnect/callback route.
+ */
+export function oidcCallbackMiddleware(
+ req: ExpressRequest,
+ res: ExpressResponse,
+ next: NextFunction,
+): void {
+ getPassport().authenticate(
+ authStrategies['openidconnect'].type,
+ (err: unknown, user: Partial, info: unknown) => {
+ if (err) {
+ console.error('Authentication error:', err);
+ return res.status(500).end();
+ }
+ if (!user) {
+ console.error('No user found:', info);
+ return res.status(401).end();
+ }
+ req.logIn(user, (err) => {
+ if (err) {
+ console.error('Login error:', err);
+ return res.status(500).end();
+ }
+ console.log('Logged in successfully. User:', user);
+ return res.redirect(`${uiHost}:${uiPort}/dashboard/profile`);
+ });
+ },
+ )(req, res, next);
+}
+
+/**
+ * Authentication endpoints.
+ */
+@Route('api/auth')
+@Tags('Auth')
+export class AuthController extends Controller {
+ /**
+ * Returns links to the available authentication resource endpoints.
+ */
+ @Get('/')
+ public getResources(): AuthResources {
+ return {
+ login: { action: 'post', uri: '/api/auth/login' },
+ profile: { action: 'get', uri: '/api/auth/profile' },
+ logout: { action: 'post', uri: '/api/auth/logout' },
+ };
+ }
+
+ /**
+ * Returns the enabled authentication methods available to the UI.
+ */
+ @Get('/config')
+ public getAuthConfig(): AuthConfigResponse {
+ const usernamePasswordMethod = getLoginStrategy();
+ return {
+ // enabled username /password auth method
+ usernamePasswordMethod,
+ // other enabled auth methods
+ otherMethods: getAuthMethods()
+ .map((am) => am.type.toLowerCase())
+ .filter((authType) => authType !== usernamePasswordMethod),
+ };
+ }
+
+ /**
+ * Authenticates the user with a username/password strategy.
+ * The appropriate passport strategy is selected dynamically based on configuration.
+ */
+ @Post('/login')
+ @Middlewares(dynamicLoginMiddleware)
+ public async login(@Request() req: ExpressRequest): Promise {
+ // dynamicLoginMiddleware has already authenticated the user and set req.user.
+ // If strategy called next(), we can log in and return the user profile.
+ const user = req.user as User;
+
+ await new Promise((resolve, reject) => {
+ req.logIn(user, (err) => (err ? reject(err) : resolve()));
+ });
+
+ const currentUser = toPublicUser(user);
+ console.log(
+ `service.routes.auth.login: user logged in, username=${currentUser.username} profile=${JSON.stringify(currentUser)}`,
+ );
+ return { message: 'success', user: currentUser };
+ }
+
+ /**
+ * Initiates the OpenID Connect authentication flow (redirects to the OIDC provider).
+ * @hidden
+ */
+ @Get('/openidconnect')
+ @Middlewares(getPassport().authenticate(authStrategies['openidconnect'].type))
+ public initiateOIDC(): void {
+ // Passport middleware handles the redirect. This body is unreachable.
+ }
+
+ /**
+ * OpenID Connect callback — exchanges the authorization code for a session.
+ * @hidden
+ */
+ @Get('/openidconnect/callback')
+ @Middlewares(oidcCallbackMiddleware)
+ public handleOIDCCallback(): void {
+ // oidcCallbackMiddleware handles login and redirect. This body is unreachable.
+ }
+
+ /**
+ * Logs out the current user and clears the session cookie.
+ */
+ @Post('/logout')
+ public async logout(@Request() req: ExpressRequest): Promise {
+ await new Promise((resolve, reject) => {
+ req.logout((err: unknown) => (err ? reject(err) : resolve()));
+ });
+ req.res?.clearCookie('connect.sid');
+ return { isAuth: req.isAuthenticated(), user: req.user };
+ }
+
+ /**
+ * Returns the profile of the currently authenticated user.
+ */
+ @Get('/profile')
+ public async getProfile(
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() notFoundResponse: NotFoundResponse,
+ ): Promise {
+ if (!req.user) {
+ return unauthorisedResponse(401, { message: 'Not logged in' });
+ }
+
+ const userVal = await db.findUser((req.user as User).username);
+ if (!userVal) {
+ return notFoundResponse(404, { message: 'User not found' });
+ }
+
+ return toPublicUser(userVal);
+ }
+
+ /**
+ * Updates the Git account (username) of a user.
+ * Admins may update any user; non-admins may only update their own account.
+ */
+ @Post('/gitAccount')
+ public async updateGitAccount(
+ @Body() body: GitAccountBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() notFoundResponse: NotFoundResponse,
+ @Res() validationErrorResponse: ValidationErrorResponse,
+ @Res() forbiddenResponse: ForbiddenResponse,
+ @Res() internalServerErrorResponse: InternalServerErrorResponse,
+ ): Promise {
+ if (!req.user) {
+ return unauthorisedResponse(401, { message: 'Not logged in' });
+ }
+
+ try {
+ let username =
+ body.username == null || body.username === 'undefined' ? body.id : body.username;
+ username = username?.split('@')[0];
+
+ if (!username) {
+ return validationErrorResponse(400, {
+ message: 'Missing username. Git account not updated',
+ });
+ }
+
+ const reqUser = await db.findUser((req.user as User).username);
+ if (username !== reqUser?.username && !reqUser?.admin) {
+ return forbiddenResponse(403, {
+ message: 'Must be an admin to update a different account',
+ });
+ }
+
+ const user = await db.findUser(username);
+ if (!user) {
+ return notFoundResponse(404, { message: 'User not found' });
+ }
+
+ user.gitAccount = body.gitAccount;
+ await db.updateUser(user);
+ this.setStatus(200);
+ } catch (error: unknown) {
+ const msg = handleErrorAndLog(error, 'Failed to update git account');
+ return internalServerErrorResponse(500, { message: msg });
+ }
+ }
+
+ /**
+ * Creates a new user. Requires admin privileges.
+ */
+ @Post('/create-user')
+ public async createUser(
+ @Body() body: CreateUserBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: ForbiddenResponse,
+ @Res() validationErrorResponse: ValidationErrorResponse,
+ @Res() internalServerErrorResponse: InternalServerErrorResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(403, { message: 'Not authorized to create users' });
+ }
+
+ const { username, password, email, gitAccount, admin: isAdmin = false } = body;
+
+ if (!username || !password || !email || !gitAccount) {
+ return validationErrorResponse(400, {
+ message: 'Missing required fields: username, password, email, and gitAccount are required',
+ });
+ }
+
+ try {
+ await db.createUser(username, password, email, gitAccount, isAdmin);
+ this.setStatus(201);
+ return { message: 'User created successfully', username };
+ } catch (error: unknown) {
+ const msg = handleErrorAndLog(error, 'Failed to create user');
+ return internalServerErrorResponse(500, { message: msg });
+ }
+ }
+}
diff --git a/src/service/controllers/ConfigController.ts b/src/service/controllers/ConfigController.ts
new file mode 100644
index 000000000..6b7e6a854
--- /dev/null
+++ b/src/service/controllers/ConfigController.ts
@@ -0,0 +1,46 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Controller, Get, Route, Tags } from 'tsoa';
+import * as config from '../../config';
+import type { AttestationConfig, UIRouteAuth } from '../../config/generated/config';
+
+/**
+ * Public configuration endpoints consumed by the UI.
+ */
+@Route('api/v1/config')
+@Tags('Config')
+export class ConfigController extends Controller {
+ @Get('/attestation')
+ public getAttestation(): AttestationConfig {
+ return config.getAttestationConfig();
+ }
+
+ @Get('/urlShortener')
+ public getUrlShortener(): string | undefined {
+ return config.getURLShortener();
+ }
+
+ @Get('/contactEmail')
+ public getContactEmail(): string | undefined {
+ return config.getContactEmail();
+ }
+
+ @Get('/uiRouteAuth')
+ public getUiRouteAuth(): UIRouteAuth {
+ return config.getUIRouteAuth();
+ }
+}
diff --git a/src/service/controllers/HealthController.ts b/src/service/controllers/HealthController.ts
new file mode 100644
index 000000000..76d8eac0b
--- /dev/null
+++ b/src/service/controllers/HealthController.ts
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Controller, Get, Route, Tags } from 'tsoa';
+import { HealthResponse } from '../interfaces/health.interfaces';
+/**
+ * Service health check.
+ */
+@Route('api/v1/healthcheck')
+@Tags('Health')
+export class HealthController extends Controller {
+ @Get('/')
+ public check(): HealthResponse {
+ return { message: 'ok' };
+ }
+}
diff --git a/src/service/controllers/HomeController.ts b/src/service/controllers/HomeController.ts
new file mode 100644
index 000000000..3f003ba13
--- /dev/null
+++ b/src/service/controllers/HomeController.ts
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Controller, Get, Route, Tags } from 'tsoa';
+import { ApiResources } from '../interfaces/home.interfaces';
+
+/**
+ * API home — lists available resource URIs.
+ */
+@Route('api')
+@Tags('Home')
+export class HomeController extends Controller {
+ @Get('/')
+ public getResources(): ApiResources {
+ return {
+ healthcheck: '/api/v1/healthcheck',
+ push: '/api/v1/push',
+ auth: '/api/auth',
+ };
+ }
+}
diff --git a/src/service/controllers/PushController.ts b/src/service/controllers/PushController.ts
new file mode 100644
index 000000000..313d931c7
--- /dev/null
+++ b/src/service/controllers/PushController.ts
@@ -0,0 +1,263 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Body, Controller, Get, Path, Post, Request, Res, Route, Security, Tags } from 'tsoa';
+import type { Request as ExpressRequest } from 'express';
+import * as db from '../../db';
+import { PushQuery } from '../../db/types';
+import { AttestationConfig } from '../../config/generated/config';
+import { getAttestationConfig } from '../../config';
+import { Action } from '../../proxy/actions/Action';
+import { AttestationAnswer, Rejection } from '../../proxy/processors/types';
+import { MessageResponse } from '../interfaces/common.interfaces';
+import { RejectBody, AuthoriseBody } from '../interfaces/push.interfaces';
+import {
+ ForbiddenResponse,
+ NotFoundResponse,
+ UnauthorisedResponse,
+ ValidationErrorResponse,
+} from '../decorators/response.types';
+
+/**
+ * Push request management.
+ */
+@Route('api/v1/push')
+@Security('jwt')
+@Tags('Push')
+export class PushController extends Controller {
+ /**
+ * Returns push requests, optionally filtered by query parameters.
+ * Supported filters: any field from PushQuery (error, blocked, allowPush, authorised, canceled, rejected, type).
+ */
+ @Get('/')
+ public async getPushes(@Request() req: ExpressRequest): Promise {
+ const query: Partial = { type: 'push' };
+
+ for (const key in req.query) {
+ if (!key) continue;
+ if (key === 'limit' || key === 'skip') continue;
+
+ const rawValue = req.query[key];
+ let parsedValue: boolean | undefined;
+ if (rawValue === 'false') parsedValue = false;
+ if (rawValue === 'true') parsedValue = true;
+ query[key] = parsedValue ?? rawValue?.toString();
+ }
+
+ return db.getPushes(query);
+ }
+
+ /**
+ * Returns a single push request by ID.
+ */
+ @Get('/{id}')
+ public async getPush(
+ @Path() id: string,
+ @Res() notFoundResponse: NotFoundResponse,
+ ): Promise {
+ const push = await db.getPush(id);
+ if (!push) {
+ return notFoundResponse(404, { message: 'not found' });
+ }
+ return push;
+ }
+
+ /**
+ * Rejects a pending push request.
+ */
+ @Post('/{id}/reject')
+ public async rejectPush(
+ @Path() id: string,
+ @Body() body: RejectBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() validationErrorResponse: ValidationErrorResponse,
+ @Res() notFoundResponse: NotFoundResponse,
+ @Res() forbiddenResponse: ForbiddenResponse,
+ ): Promise {
+ if (!req.user) {
+ return unauthorisedResponse(401, { message: 'Not logged in' });
+ }
+
+ const { reason } = body;
+ if (!reason || !reason.trim()) {
+ return validationErrorResponse(400, { message: 'Rejection reason is required' });
+ }
+
+ const { username } = req.user as { username: string };
+
+ const push = await db.getPush(id);
+ if (!push) {
+ return notFoundResponse(404, { message: 'Push request not found' });
+ }
+
+ if (!push.userEmail) {
+ return validationErrorResponse(400, { message: 'Push request has no user email' });
+ }
+
+ const committerEmail = push.userEmail;
+ const list = await db.getUsers({ email: committerEmail });
+
+ if (list.length === 0) {
+ return notFoundResponse(404, {
+ message: `No user found with the committer's email address: ${committerEmail}`,
+ });
+ }
+
+ if (list[0].username.toLowerCase() === username.toLowerCase() && !list[0].admin) {
+ return forbiddenResponse(403, { message: 'Cannot reject your own changes' });
+ }
+
+ const isAllowed = await db.canUserApproveRejectPush(id, username);
+ if (!isAllowed) {
+ return forbiddenResponse(403, {
+ message: `User ${username} is not authorised to reject changes on this project`,
+ });
+ }
+
+ const reviewerList = await db.getUsers({ username });
+ const reviewerEmail = reviewerList[0].email;
+
+ if (!reviewerEmail) {
+ return notFoundResponse(404, {
+ message: `There was no registered email address for the reviewer: ${username}`,
+ });
+ }
+
+ const rejection: Rejection = {
+ reason,
+ timestamp: new Date(),
+ reviewer: { username, email: reviewerEmail },
+ };
+
+ const result = await db.reject(id, rejection);
+ console.log(
+ `User ${username} rejected push request for ${id}${reason ? ` with reason: ${reason}` : ''}`,
+ );
+ return result;
+ }
+
+ /**
+ * Authorises (approves) a pending push request.
+ */
+ @Post('/{id}/authorise')
+ public async authorisePush(
+ @Path() id: string,
+ @Body() body: AuthoriseBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() validationErrorResponse: ValidationErrorResponse,
+ @Res() notFoundResponse: NotFoundResponse,
+ @Res() forbiddenResponse: ForbiddenResponse,
+ ): Promise {
+ if (!req.user) {
+ return unauthorisedResponse(401, { message: 'Not logged in' });
+ }
+
+ const answers = body.params.attestation;
+ if (!validateAttestation(answers, getAttestationConfig())) {
+ return validationErrorResponse(400, { message: 'Attestation is not complete' });
+ }
+
+ const { username } = req.user as { username: string };
+
+ const push = await db.getPush(id);
+ if (!push) {
+ return notFoundResponse(404, { message: 'Push request not found' });
+ }
+
+ // Get the committer of the push via their email address
+ const committerEmail = push.userEmail;
+ const list = await db.getUsers({ email: committerEmail });
+
+ if (list.length === 0) {
+ return notFoundResponse(404, {
+ message: `No user found with the committer's email address: ${committerEmail}`,
+ });
+ }
+
+ if (list[0].username.toLowerCase() === username.toLowerCase() && !list[0].admin) {
+ return forbiddenResponse(403, { message: 'Cannot approve your own changes' });
+ }
+
+ // If we are not the author, now check that we are allowed to authorise on this
+ // repo
+ const isAllowed = await db.canUserApproveRejectPush(id, username);
+ if (!isAllowed) {
+ return forbiddenResponse(403, {
+ message: `User ${username} not authorised to approve pushes on this project`,
+ });
+ }
+
+ const reviewerList = await db.getUsers({ username });
+ const reviewerEmail = reviewerList[0].email;
+
+ if (!reviewerEmail) {
+ return notFoundResponse(404, {
+ message: `There was no registered email address for the reviewer: ${username}`,
+ });
+ }
+
+ const attestation = {
+ answers,
+ timestamp: new Date(),
+ reviewer: { username, email: reviewerEmail },
+ };
+
+ console.log(`User ${username} approved push request for ${id}`);
+ return db.authorise(id, attestation);
+ }
+
+ /**
+ * Cancels a pending push request.
+ */
+ @Post('/{id}/cancel')
+ public async cancelPush(
+ @Path() id: string,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() forbiddenResponse: ForbiddenResponse,
+ ): Promise {
+ if (!req.user) {
+ return unauthorisedResponse(401, { message: 'Not logged in' });
+ }
+
+ const { username } = req.user as { username: string };
+ const isAllowed = await db.canUserCancelPush(id, username);
+
+ if (!isAllowed) {
+ console.log(`User ${username} not authorised to cancel push request for ${id}`);
+ return forbiddenResponse(403, {
+ message: `User ${username} not authorised to cancel push requests on this project`,
+ });
+ }
+
+ const result = await db.cancel(id);
+ console.log(`User ${username} canceled push request for ${id}`);
+ return result;
+ }
+}
+
+function validateAttestation(answers: AttestationAnswer[], config: AttestationConfig): boolean {
+ const configQuestions = config.questions ?? [];
+
+ if (!answers || answers.length !== configQuestions.length) {
+ return false;
+ }
+
+ const configLabels = new Set(configQuestions.map((q) => q.label));
+ return answers.every((answer) => configLabels.has(answer.label) && !!answer.checked);
+}
diff --git a/src/service/controllers/RepoController.ts b/src/service/controllers/RepoController.ts
new file mode 100644
index 000000000..d6551f465
--- /dev/null
+++ b/src/service/controllers/RepoController.ts
@@ -0,0 +1,293 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ Body,
+ Controller,
+ Delete,
+ Get,
+ Patch,
+ Path,
+ Post,
+ Request,
+ Res,
+ Route,
+ Security,
+ Tags,
+} from 'tsoa';
+import type { Request as ExpressRequest } from 'express';
+import * as db from '../../db';
+import { RepoQuery } from '../../db/types';
+import { getProxyURL } from '../urls';
+import { isAdminUser } from '../routes/utils';
+import { getProxy } from '../proxyStore';
+import { handleErrorAndLog } from '../../utils/errors';
+import { MessageResponse } from '../interfaces/common.interfaces';
+import { UsernameBody, CreateRepoBody, RepoWithProxy } from '../interfaces/repo.interfaces';
+import {
+ ConflictResponse,
+ InternalServerErrorResponse,
+ NotFoundResponse,
+ UnauthorisedResponse,
+ UserNotFoundResponse,
+ ValidationErrorResponse,
+} from '../decorators/response.types';
+
+/**
+ * Repository management.
+ */
+@Route('api/v1/repo')
+@Security('jwt')
+@Tags('Repositories')
+export class RepoController extends Controller {
+ /**
+ * Returns repositories, optionally filtered by query parameters.
+ */
+ @Get('/')
+ public async getRepos(@Request() req: ExpressRequest): Promise {
+ const proxyURL = getProxyURL(req);
+ const query: Partial = {};
+
+ for (const key in req.query) {
+ if (!key) continue;
+ if (key === 'limit' || key === 'skip') continue;
+
+ const rawValue = req.query[key];
+ let parsedValue: boolean | undefined;
+ if (rawValue === 'false') parsedValue = false;
+ if (rawValue === 'true') parsedValue = true;
+ query[key] = parsedValue ?? rawValue?.toString();
+ }
+
+ const repos = await db.getRepos(query);
+ return repos.map((d) => ({ ...d, proxyURL }));
+ }
+
+ /**
+ * Returns a single repository by ID.
+ */
+ @Get('/{id}')
+ public async getRepo(
+ @Path() id: string,
+ @Request() req: ExpressRequest,
+ @Res() notFoundResponse: NotFoundResponse,
+ ): Promise {
+ const proxyURL = getProxyURL(req);
+ const repo = await db.getRepoById(id);
+ if (!repo) {
+ return notFoundResponse(404, { message: `Repository ${id} not found` });
+ }
+ return { ...repo, proxyURL };
+ }
+
+ /**
+ * Creates a new repository. May restart the proxy if a new origin is added.
+ */
+ @Post('/')
+ public async createRepo(
+ @Body() body: CreateRepoBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() validationErrorResponse: ValidationErrorResponse,
+ @Res() conflictResponse: ConflictResponse,
+ @Res() internalServerErrorResponse: InternalServerErrorResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ const repoUrl = body.url;
+
+ if (!repoUrl) {
+ return validationErrorResponse(400, { message: 'Repository url is required' });
+ }
+
+ const existing = await db.getRepoByUrl(repoUrl);
+ if (existing) {
+ return conflictResponse(409, { message: `Repository ${repoUrl} already exists!` });
+ }
+
+ try {
+ // figure out if this represent a new domain to proxy
+ let newOrigin = true;
+
+ const existingHosts = await db.getAllProxiedHosts();
+ existingHosts.forEach((h) => {
+ // assume SSL is in use and that our origins are missing the protocol
+ if (repoUrl.startsWith(`https://${h}`)) {
+ newOrigin = false;
+ }
+ });
+
+ console.log(
+ `API request to proxy repository ${repoUrl} is for a new origin: ${newOrigin},\n\texisting origin list was: ${JSON.stringify(existingHosts)}`,
+ );
+
+ const repoDetails = await db.createRepo(body);
+ const proxyURL = getProxyURL(req);
+
+ // restart the proxy if we're proxying a new domain
+ if (newOrigin) {
+ console.log('Restarting the proxy to handle an additional host');
+ const proxy = getProxy();
+ await proxy.stop();
+ await proxy.start();
+ }
+
+ return { ...repoDetails, proxyURL, message: 'created' };
+ } catch (error: unknown) {
+ const msg = handleErrorAndLog(error, 'Repository creation failed');
+ return internalServerErrorResponse(500, { message: msg });
+ }
+ }
+
+ /**
+ * Grants a user push permission on a repository.
+ */
+ @Patch('/{id}/user/push')
+ public async addPushUser(
+ @Path() id: string,
+ @Body() body: UsernameBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() userNotFoundResponse: UserNotFoundResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ const username = body.username.toLowerCase();
+ const user = await db.findUser(username);
+ if (!user) {
+ return userNotFoundResponse(400, { error: 'User does not exist' });
+ }
+
+ await db.addUserCanPush(id, username);
+ return { message: 'created' };
+ }
+
+ /**
+ * Grants a user authorise permission on a repository.
+ */
+ @Patch('/{id}/user/authorise')
+ public async addAuthoriseUser(
+ @Path() id: string,
+ @Body() body: UsernameBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() userNotFoundResponse: UserNotFoundResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ const user = await db.findUser(body.username);
+ if (!user) {
+ return userNotFoundResponse(400, { error: 'User does not exist' });
+ }
+
+ await db.addUserCanAuthorise(id, body.username);
+ return { message: 'created' };
+ }
+
+ /**
+ * Revokes a user's authorise permission on a repository.
+ */
+ @Delete('/{id}/user/authorise/{username}')
+ public async removeAuthoriseUser(
+ @Path() id: string,
+ @Path() username: string,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() userNotFoundResponse: UserNotFoundResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ const user = await db.findUser(username);
+ if (!user) {
+ return userNotFoundResponse(400, { error: 'User does not exist' });
+ }
+
+ await db.removeUserCanAuthorise(id, username);
+ return { message: 'created' };
+ }
+
+ /**
+ * Revokes a user's push permission on a repository.
+ */
+ @Delete('/{id}/user/push/{username}')
+ public async removePushUser(
+ @Path() id: string,
+ @Path() username: string,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() userNotFoundResponse: UserNotFoundResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ const user = await db.findUser(username);
+ if (!user) {
+ return userNotFoundResponse(400, { error: 'User does not exist' });
+ }
+
+ await db.removeUserCanPush(id, username);
+ return { message: 'created' };
+ }
+
+ /**
+ * Deletes a repository. May restart the proxy if a proxied host is removed.
+ */
+ @Delete('/{id}/delete')
+ public async deleteRepo(
+ @Path() id: string,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ // determine if we need to restart the proxy
+ const previousHosts = await db.getAllProxiedHosts();
+ await db.deleteRepo(id);
+ const currentHosts = await db.getAllProxiedHosts();
+
+ if (currentHosts.length < previousHosts.length) {
+ console.log('Restarting the proxy to remove a host');
+ const proxy = getProxy();
+ await proxy.stop();
+ await proxy.start();
+ }
+
+ return { message: 'deleted' };
+ }
+}
diff --git a/src/service/controllers/UserController.ts b/src/service/controllers/UserController.ts
new file mode 100644
index 000000000..82e596b59
--- /dev/null
+++ b/src/service/controllers/UserController.ts
@@ -0,0 +1,56 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Controller, Get, Path, Res, Route, Security, Tags } from 'tsoa';
+import * as db from '../../db';
+import { PublicUser } from '../../db/types';
+import { toPublicUser } from '../routes/utils';
+import { NotFoundResponse } from '../decorators/response.types';
+
+/**
+ * User listing.
+ */
+@Route('api/v1/user')
+@Security('jwt')
+@Tags('Users')
+export class UserController extends Controller {
+ /**
+ * Returns all registered users (public fields only).
+ */
+ @Get('/')
+ public async getUsers(): Promise {
+ console.log('fetching users');
+ const users = await db.getUsers();
+ return users.map(toPublicUser);
+ }
+
+ /**
+ * Returns a single user by username.
+ */
+ @Get('/{id}')
+ public async getUser(
+ @Path() id: string,
+ @Res() notFoundResponse: NotFoundResponse,
+ ): Promise {
+ const username = id.toLowerCase();
+ console.log(`Retrieving details for user: ${username}`);
+ const user = await db.findUser(username);
+ if (!user) {
+ return notFoundResponse(404, { message: `User ${username} not found` });
+ }
+ return toPublicUser(user);
+ }
+}
diff --git a/src/service/decorators/response.types.ts b/src/service/decorators/response.types.ts
new file mode 100644
index 000000000..478a145dd
--- /dev/null
+++ b/src/service/decorators/response.types.ts
@@ -0,0 +1,25 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { TsoaResponse } from 'tsoa';
+
+export type UnauthorisedResponse = TsoaResponse<401, { message: string }>;
+export type ForbiddenResponse = TsoaResponse<403, { message: string }>;
+export type NotFoundResponse = TsoaResponse<404, { message: string }>;
+export type ValidationErrorResponse = TsoaResponse<400, { message: string }>;
+export type ConflictResponse = TsoaResponse<409, { message: string }>;
+export type InternalServerErrorResponse = TsoaResponse<500, { message: string }>;
+export type UserNotFoundResponse = TsoaResponse<400, { error: string }>;
diff --git a/src/service/generatedRoutes.ts b/src/service/generatedRoutes.ts
new file mode 100644
index 000000000..588635686
--- /dev/null
+++ b/src/service/generatedRoutes.ts
@@ -0,0 +1,1382 @@
+/* tslint:disable */
+/* eslint-disable */
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import type { TsoaRoute } from '@tsoa/runtime';
+import { fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { UserController } from './controllers/UserController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { RepoController } from './controllers/RepoController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { PushController } from './controllers/PushController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { HomeController } from './controllers/HomeController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { HealthController } from './controllers/HealthController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { ConfigController } from './controllers/ConfigController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { AuthController } from './controllers/AuthController';
+import { expressAuthentication } from './authentication';
+// @ts-ignore - no great way to install types from subpackage
+import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express';
+
+const expressAuthenticationRecasted = expressAuthentication as (req: ExRequest, securityName: string, scopes?: string[], res?: ExResponse) => Promise;
+
+
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+const models: TsoaRoute.Models = {
+ "PublicUser": {
+ "dataType": "refObject",
+ "properties": {
+ "username": {"dataType":"string","required":true},
+ "displayName": {"dataType":"string","required":true},
+ "email": {"dataType":"string","required":true},
+ "title": {"dataType":"string","required":true},
+ "gitAccount": {"dataType":"string","required":true},
+ "admin": {"dataType":"boolean","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "RepoWithProxy": {
+ "dataType": "refObject",
+ "properties": {
+ "project": {"dataType":"string","required":true},
+ "name": {"dataType":"string","required":true},
+ "url": {"dataType":"string","required":true},
+ "users": {"dataType":"nestedObjectLiteral","nestedProperties":{"canAuthorise":{"dataType":"array","array":{"dataType":"string"},"required":true},"canPush":{"dataType":"array","array":{"dataType":"string"},"required":true}},"required":true},
+ "_id": {"dataType":"string"},
+ "proxyURL": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "MessageResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "message": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "CreateRepoBody": {
+ "dataType": "refObject",
+ "properties": {
+ "url": {"dataType":"string","required":true},
+ "name": {"dataType":"string","required":true},
+ "project": {"dataType":"string","required":true},
+ "users": {"dataType":"nestedObjectLiteral","nestedProperties":{"canAuthorise":{"dataType":"array","array":{"dataType":"string"},"required":true},"canPush":{"dataType":"array","array":{"dataType":"string"},"required":true}}},
+ "_id": {"dataType":"string"},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "UsernameBody": {
+ "dataType": "refObject",
+ "properties": {
+ "username": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Step": {
+ "dataType": "refObject",
+ "properties": {
+ "id": {"dataType":"string","required":true},
+ "stepName": {"dataType":"string","required":true},
+ "content": {"dataType":"any","required":true},
+ "error": {"dataType":"boolean","required":true},
+ "errorMessage": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},
+ "blocked": {"dataType":"boolean","required":true},
+ "blockedMessage": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},
+ "logs": {"dataType":"array","array":{"dataType":"string"},"default":[]},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "CommitData": {
+ "dataType": "refAlias",
+ "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true},"commitTimestamp":{"dataType":"string","required":true},"committerEmail":{"dataType":"string","required":true},"authorEmail":{"dataType":"string","required":true},"committer":{"dataType":"string","required":true},"author":{"dataType":"string","required":true},"parent":{"dataType":"string","required":true},"tree":{"dataType":"string","required":true}},"validators":{}},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AttestationBase": {
+ "dataType": "refAlias",
+ "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"automated":{"dataType":"boolean"},"timestamp":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true},"reviewer":{"dataType":"nestedObjectLiteral","nestedProperties":{"email":{"dataType":"string","required":true},"username":{"dataType":"string","required":true}},"required":true}},"validators":{}},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AttestationAnswer": {
+ "dataType": "refObject",
+ "properties": {
+ "label": {"dataType":"string","required":true},
+ "checked": {"dataType":"boolean","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "CompletedAttestation": {
+ "dataType": "refAlias",
+ "type": {"dataType":"intersection","subSchemas":[{"ref":"AttestationBase"},{"dataType":"nestedObjectLiteral","nestedProperties":{"answers":{"dataType":"array","array":{"dataType":"refObject","ref":"AttestationAnswer"},"required":true}}}],"validators":{}},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Rejection": {
+ "dataType": "refAlias",
+ "type": {"dataType":"intersection","subSchemas":[{"ref":"AttestationBase"},{"dataType":"nestedObjectLiteral","nestedProperties":{"reason":{"dataType":"string","required":true}}}],"validators":{}},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Action": {
+ "dataType": "refObject",
+ "properties": {
+ "id": {"dataType":"string","required":true},
+ "type": {"dataType":"string","required":true},
+ "method": {"dataType":"string","required":true},
+ "timestamp": {"dataType":"double","required":true},
+ "project": {"dataType":"string","required":true},
+ "repoName": {"dataType":"string","required":true},
+ "url": {"dataType":"string","required":true},
+ "repo": {"dataType":"string","required":true},
+ "steps": {"dataType":"array","array":{"dataType":"refObject","ref":"Step"},"default":[]},
+ "error": {"dataType":"boolean","default":false},
+ "errorMessage": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]},
+ "blocked": {"dataType":"boolean","default":false},
+ "blockedMessage": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]},
+ "allowPush": {"dataType":"boolean","default":false},
+ "authorised": {"dataType":"boolean","default":false},
+ "canceled": {"dataType":"boolean","default":false},
+ "rejected": {"dataType":"boolean","default":false},
+ "autoApproved": {"dataType":"boolean","default":false},
+ "autoRejected": {"dataType":"boolean","default":false},
+ "commitData": {"dataType":"array","array":{"dataType":"refAlias","ref":"CommitData"},"default":[]},
+ "commitFrom": {"dataType":"string"},
+ "commitTo": {"dataType":"string"},
+ "branch": {"dataType":"string"},
+ "message": {"dataType":"string"},
+ "author": {"dataType":"string"},
+ "user": {"dataType":"string"},
+ "userEmail": {"dataType":"string"},
+ "attestation": {"ref":"CompletedAttestation"},
+ "rejection": {"ref":"Rejection"},
+ "lastStep": {"ref":"Step"},
+ "proxyGitPath": {"dataType":"string"},
+ "newIdxFiles": {"dataType":"array","array":{"dataType":"string"}},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "RejectBody": {
+ "dataType": "refObject",
+ "properties": {
+ "reason": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AuthoriseBody": {
+ "dataType": "refObject",
+ "properties": {
+ "params": {"dataType":"nestedObjectLiteral","nestedProperties":{"attestation":{"dataType":"array","array":{"dataType":"refObject","ref":"AttestationAnswer"},"required":true}},"required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "ApiResources": {
+ "dataType": "refObject",
+ "properties": {
+ "healthcheck": {"dataType":"string","required":true},
+ "push": {"dataType":"string","required":true},
+ "auth": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "HealthResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "message": {"dataType":"enum","enums":["ok"],"required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Link": {
+ "dataType": "refObject",
+ "properties": {
+ "text": {"dataType":"string","required":true},
+ "url": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "QuestionTooltip": {
+ "dataType": "refObject",
+ "properties": {
+ "links": {"dataType":"array","array":{"dataType":"refObject","ref":"Link"}},
+ "text": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Question": {
+ "dataType": "refObject",
+ "properties": {
+ "label": {"dataType":"string","required":true},
+ "tooltip": {"ref":"QuestionTooltip","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AttestationConfig": {
+ "dataType": "refObject",
+ "properties": {
+ "questions": {"dataType":"array","array":{"dataType":"refObject","ref":"Question"}},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "RouteAuthRule": {
+ "dataType": "refObject",
+ "properties": {
+ "adminOnly": {"dataType":"boolean"},
+ "loginRequired": {"dataType":"boolean"},
+ "pattern": {"dataType":"string"},
+ },
+ "additionalProperties": {"dataType":"any"},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "UIRouteAuth": {
+ "dataType": "refObject",
+ "properties": {
+ "enabled": {"dataType":"boolean"},
+ "rules": {"dataType":"array","array":{"dataType":"refObject","ref":"RouteAuthRule"}},
+ },
+ "additionalProperties": {"dataType":"any"},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AuthResources": {
+ "dataType": "refObject",
+ "properties": {
+ "login": {"dataType":"nestedObjectLiteral","nestedProperties":{"uri":{"dataType":"string","required":true},"action":{"dataType":"enum","enums":["post"],"required":true}},"required":true},
+ "profile": {"dataType":"nestedObjectLiteral","nestedProperties":{"uri":{"dataType":"string","required":true},"action":{"dataType":"enum","enums":["get"],"required":true}},"required":true},
+ "logout": {"dataType":"nestedObjectLiteral","nestedProperties":{"uri":{"dataType":"string","required":true},"action":{"dataType":"enum","enums":["post"],"required":true}},"required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AuthConfigResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "usernamePasswordMethod": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},
+ "otherMethods": {"dataType":"array","array":{"dataType":"string"},"required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "LoginResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "message": {"dataType":"enum","enums":["success"],"required":true},
+ "user": {"ref":"PublicUser","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Express.User": {
+ "dataType": "refObject",
+ "properties": {
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "LogoutResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "isAuth": {"dataType":"boolean","required":true},
+ "user": {"dataType":"union","subSchemas":[{"ref":"Express.User"},{"dataType":"undefined"}],"required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "GitAccountBody": {
+ "dataType": "refObject",
+ "properties": {
+ "username": {"dataType":"string"},
+ "id": {"dataType":"string"},
+ "gitAccount": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "CreateUserResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "message": {"dataType":"string","required":true},
+ "username": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "CreateUserBody": {
+ "dataType": "refObject",
+ "properties": {
+ "username": {"dataType":"string","required":true},
+ "password": {"dataType":"string","required":true},
+ "email": {"dataType":"string","required":true},
+ "gitAccount": {"dataType":"string","required":true},
+ "admin": {"dataType":"boolean"},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+};
+const templateService = new ExpressTemplateService(models, {"noImplicitAdditionalProperties":"throw-on-extras","bodyCoercion":true});
+
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+
+
+
+export function RegisterRoutes(app: Router) {
+
+ // ###########################################################################################################
+ // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look
+ // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa
+ // ###########################################################################################################
+
+
+
+ const argsUserController_getUsers: Record = {
+ };
+ app.get('/api/v1/user',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(UserController)),
+ ...(fetchMiddlewares(UserController.prototype.getUsers)),
+
+ async function UserController_getUsers(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsUserController_getUsers, request, response });
+
+ const controller = new UserController();
+
+ await templateService.apiHandler({
+ methodName: 'getUsers',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsUserController_getUser: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.get('/api/v1/user/:id',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(UserController)),
+ ...(fetchMiddlewares(UserController.prototype.getUser)),
+
+ async function UserController_getUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsUserController_getUser, request, response });
+
+ const controller = new UserController();
+
+ await templateService.apiHandler({
+ methodName: 'getUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_getRepos: Record = {
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ };
+ app.get('/api/v1/repo',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.getRepos)),
+
+ async function RepoController_getRepos(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_getRepos, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'getRepos',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_getRepo: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.get('/api/v1/repo/:id',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.getRepo)),
+
+ async function RepoController_getRepo(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_getRepo, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'getRepo',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_createRepo: Record = {
+ body: {"in":"body","name":"body","required":true,"ref":"CreateRepoBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ validationErrorResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ conflictResponse: {"in":"res","name":"409","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ internalServerErrorResponse: {"in":"res","name":"500","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/v1/repo',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.createRepo)),
+
+ async function RepoController_createRepo(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_createRepo, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'createRepo',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_addPushUser: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ body: {"in":"body","name":"body","required":true,"ref":"UsernameBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ userNotFoundResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ };
+ app.patch('/api/v1/repo/:id/user/push',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.addPushUser)),
+
+ async function RepoController_addPushUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_addPushUser, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'addPushUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_addAuthoriseUser: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ body: {"in":"body","name":"body","required":true,"ref":"UsernameBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ userNotFoundResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ };
+ app.patch('/api/v1/repo/:id/user/authorise',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.addAuthoriseUser)),
+
+ async function RepoController_addAuthoriseUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_addAuthoriseUser, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'addAuthoriseUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_removeAuthoriseUser: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ username: {"in":"path","name":"username","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ userNotFoundResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ };
+ app.delete('/api/v1/repo/:id/user/authorise/:username',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.removeAuthoriseUser)),
+
+ async function RepoController_removeAuthoriseUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_removeAuthoriseUser, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'removeAuthoriseUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_removePushUser: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ username: {"in":"path","name":"username","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ userNotFoundResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ };
+ app.delete('/api/v1/repo/:id/user/push/:username',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.removePushUser)),
+
+ async function RepoController_removePushUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_removePushUser, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'removePushUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_deleteRepo: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.delete('/api/v1/repo/:id/delete',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.deleteRepo)),
+
+ async function RepoController_deleteRepo(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_deleteRepo, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'deleteRepo',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsPushController_getPushes: Record = {
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ };
+ app.get('/api/v1/push',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(PushController)),
+ ...(fetchMiddlewares(PushController.prototype.getPushes)),
+
+ async function PushController_getPushes(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsPushController_getPushes, request, response });
+
+ const controller = new PushController();
+
+ await templateService.apiHandler({
+ methodName: 'getPushes',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsPushController_getPush: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.get('/api/v1/push/:id',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(PushController)),
+ ...(fetchMiddlewares(PushController.prototype.getPush)),
+
+ async function PushController_getPush(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsPushController_getPush, request, response });
+
+ const controller = new PushController();
+
+ await templateService.apiHandler({
+ methodName: 'getPush',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsPushController_rejectPush: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ body: {"in":"body","name":"body","required":true,"ref":"RejectBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ validationErrorResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ forbiddenResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/v1/push/:id/reject',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(PushController)),
+ ...(fetchMiddlewares(PushController.prototype.rejectPush)),
+
+ async function PushController_rejectPush(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsPushController_rejectPush, request, response });
+
+ const controller = new PushController();
+
+ await templateService.apiHandler({
+ methodName: 'rejectPush',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsPushController_authorisePush: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ body: {"in":"body","name":"body","required":true,"ref":"AuthoriseBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ validationErrorResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ forbiddenResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/v1/push/:id/authorise',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(PushController)),
+ ...(fetchMiddlewares(PushController.prototype.authorisePush)),
+
+ async function PushController_authorisePush(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsPushController_authorisePush, request, response });
+
+ const controller = new PushController();
+
+ await templateService.apiHandler({
+ methodName: 'authorisePush',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsPushController_cancelPush: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ forbiddenResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/v1/push/:id/cancel',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(PushController)),
+ ...(fetchMiddlewares(PushController.prototype.cancelPush)),
+
+ async function PushController_cancelPush(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsPushController_cancelPush, request, response });
+
+ const controller = new PushController();
+
+ await templateService.apiHandler({
+ methodName: 'cancelPush',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsHomeController_getResources: Record = {
+ };
+ app.get('/api',
+ ...(fetchMiddlewares(HomeController)),
+ ...(fetchMiddlewares(HomeController.prototype.getResources)),
+
+ async function HomeController_getResources(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsHomeController_getResources, request, response });
+
+ const controller = new HomeController();
+
+ await templateService.apiHandler({
+ methodName: 'getResources',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsHealthController_check: Record = {
+ };
+ app.get('/api/v1/healthcheck',
+ ...(fetchMiddlewares(HealthController)),
+ ...(fetchMiddlewares(HealthController.prototype.check)),
+
+ async function HealthController_check(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsHealthController_check, request, response });
+
+ const controller = new HealthController();
+
+ await templateService.apiHandler({
+ methodName: 'check',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsConfigController_getAttestation: Record = {
+ };
+ app.get('/api/v1/config/attestation',
+ ...(fetchMiddlewares(ConfigController)),
+ ...(fetchMiddlewares(ConfigController.prototype.getAttestation)),
+
+ async function ConfigController_getAttestation(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsConfigController_getAttestation, request, response });
+
+ const controller = new ConfigController();
+
+ await templateService.apiHandler({
+ methodName: 'getAttestation',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsConfigController_getUrlShortener: Record = {
+ };
+ app.get('/api/v1/config/urlShortener',
+ ...(fetchMiddlewares(ConfigController)),
+ ...(fetchMiddlewares(ConfigController.prototype.getUrlShortener)),
+
+ async function ConfigController_getUrlShortener(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsConfigController_getUrlShortener, request, response });
+
+ const controller = new ConfigController();
+
+ await templateService.apiHandler({
+ methodName: 'getUrlShortener',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsConfigController_getContactEmail: Record = {
+ };
+ app.get('/api/v1/config/contactEmail',
+ ...(fetchMiddlewares(ConfigController)),
+ ...(fetchMiddlewares(ConfigController.prototype.getContactEmail)),
+
+ async function ConfigController_getContactEmail(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsConfigController_getContactEmail, request, response });
+
+ const controller = new ConfigController();
+
+ await templateService.apiHandler({
+ methodName: 'getContactEmail',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsConfigController_getUiRouteAuth: Record = {
+ };
+ app.get('/api/v1/config/uiRouteAuth',
+ ...(fetchMiddlewares(ConfigController)),
+ ...(fetchMiddlewares(ConfigController.prototype.getUiRouteAuth)),
+
+ async function ConfigController_getUiRouteAuth(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsConfigController_getUiRouteAuth, request, response });
+
+ const controller = new ConfigController();
+
+ await templateService.apiHandler({
+ methodName: 'getUiRouteAuth',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_getResources: Record = {
+ };
+ app.get('/api/auth',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.getResources)),
+
+ async function AuthController_getResources(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_getResources, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'getResources',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_getAuthConfig: Record = {
+ };
+ app.get('/api/auth/config',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.getAuthConfig)),
+
+ async function AuthController_getAuthConfig(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_getAuthConfig, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'getAuthConfig',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_login: Record = {
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ };
+ app.post('/api/auth/login',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.login)),
+
+ async function AuthController_login(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_login, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'login',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_initiateOIDC: Record = {
+ };
+ app.get('/api/auth/openidconnect',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.initiateOIDC)),
+
+ async function AuthController_initiateOIDC(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_initiateOIDC, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'initiateOIDC',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_handleOIDCCallback: Record = {
+ };
+ app.get('/api/auth/openidconnect/callback',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.handleOIDCCallback)),
+
+ async function AuthController_handleOIDCCallback(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_handleOIDCCallback, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'handleOIDCCallback',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_logout: Record = {
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ };
+ app.post('/api/auth/logout',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.logout)),
+
+ async function AuthController_logout(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_logout, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'logout',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_getProfile: Record = {
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.get('/api/auth/profile',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.getProfile)),
+
+ async function AuthController_getProfile(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_getProfile, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'getProfile',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_updateGitAccount: Record = {
+ body: {"in":"body","name":"body","required":true,"ref":"GitAccountBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ validationErrorResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ forbiddenResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ internalServerErrorResponse: {"in":"res","name":"500","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/auth/gitAccount',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.updateGitAccount)),
+
+ async function AuthController_updateGitAccount(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_updateGitAccount, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'updateGitAccount',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_createUser: Record = {
+ body: {"in":"body","name":"body","required":true,"ref":"CreateUserBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ validationErrorResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ internalServerErrorResponse: {"in":"res","name":"500","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/auth/create-user',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.createUser)),
+
+ async function AuthController_createUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_createUser, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'createUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ function authenticateMiddleware(security: TsoaRoute.Security[] = []) {
+ return async function runAuthenticationMiddleware(request: any, response: any, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ // keep track of failed auth attempts so we can hand back the most
+ // recent one. This behavior was previously existing so preserving it
+ // here
+ const failedAttempts: any[] = [];
+ const pushAndRethrow = (error: any) => {
+ failedAttempts.push(error);
+ throw error;
+ };
+
+ const secMethodOrPromises: Promise[] = [];
+ for (const secMethod of security) {
+ if (Object.keys(secMethod).length > 1) {
+ const secMethodAndPromises: Promise[] = [];
+
+ for (const name in secMethod) {
+ secMethodAndPromises.push(
+ expressAuthenticationRecasted(request, name, secMethod[name], response)
+ .catch(pushAndRethrow)
+ );
+ }
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ secMethodOrPromises.push(Promise.all(secMethodAndPromises)
+ .then(users => { return users[0]; }));
+ } else {
+ for (const name in secMethod) {
+ secMethodOrPromises.push(
+ expressAuthenticationRecasted(request, name, secMethod[name], response)
+ .catch(pushAndRethrow)
+ );
+ }
+ }
+ }
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ try {
+ request['user'] = await Promise.any(secMethodOrPromises);
+
+ // Response was sent in middleware, abort
+ if (response.writableEnded) {
+ return;
+ }
+
+ next();
+ }
+ catch(err) {
+ // Show most recent error as response
+ const error = failedAttempts.pop();
+ error.status = error.status || 401;
+
+ // Response was sent in middleware, abort
+ if (response.writableEnded) {
+ return;
+ }
+ next(error);
+ }
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ }
+ }
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+}
+
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
diff --git a/src/service/index.ts b/src/service/index.ts
index 1053c58eb..d544ca037 100644
--- a/src/service/index.ts
+++ b/src/service/index.ts
@@ -28,8 +28,11 @@ import * as config from '../config';
import * as db from '../db';
import { serverConfig } from '../config/env';
import { Proxy } from '../proxy';
-import routes from './routes';
+import { RegisterRoutes } from './generatedRoutes';
+import { setProxy } from './proxyStore';
import { configure } from './passport';
+import { ValidateError } from 'tsoa';
+import type { Request, Response, NextFunction } from 'express';
const limiter = rateLimit(config.getRateLimit());
@@ -177,7 +180,33 @@ async function createApp(proxy: Proxy): Promise {
app.use(passport.session());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
- app.use('/', routes(proxy));
+
+ // Make the Proxy instance available to controllers before registering routes.
+ if (proxy) setProxy(proxy);
+ RegisterRoutes(app);
+
+ // Error handlers — must be registered after routes.
+ // Handle tsoa validation errors (missing/invalid fields).
+ app.use((err: unknown, _req: Request, res: Response, next: NextFunction) => {
+ if (err instanceof ValidateError) {
+ return res.status(400).json({
+ message: 'Validation failed',
+ details: err.fields,
+ });
+ }
+ next(err);
+ });
+ // Handle HTTP errors thrown from controllers (objects with a .status property).
+ app.use((err: unknown, _req: Request, res: Response, next: NextFunction) => {
+ const httpErr = err as { status?: number; message?: string };
+ const status = typeof httpErr.status === 'number' ? httpErr.status : 500;
+ const message = httpErr.message ?? 'Internal server error';
+ if (!res.headersSent) {
+ return res.status(status).json({ message });
+ }
+ next(err);
+ });
+
app.use('/', express.static(absBuildPath));
app.get('/*path', (_req, res) => {
res.sendFile(path.join(`${absBuildPath}/index.html`));
diff --git a/src/service/interfaces/auth.interfaces.ts b/src/service/interfaces/auth.interfaces.ts
new file mode 100644
index 000000000..6a00f490f
--- /dev/null
+++ b/src/service/interfaces/auth.interfaces.ts
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { PublicUser } from '../../db/types';
+
+export interface AuthResources {
+ login: { action: 'post'; uri: string };
+ profile: { action: 'get'; uri: string };
+ logout: { action: 'post'; uri: string };
+}
+
+export interface AuthConfigResponse {
+ usernamePasswordMethod: string | null;
+ otherMethods: string[];
+}
+
+export interface LoginResponse {
+ message: 'success';
+ user: PublicUser;
+}
+
+export interface LogoutResponse {
+ isAuth: boolean;
+ user: Express.User | undefined;
+}
+
+export interface CreateUserResponse {
+ message: string;
+ username: string;
+}
+
+export interface GitAccountBody {
+ username?: string;
+ id?: string;
+ gitAccount: string;
+}
+
+export interface CreateUserBody {
+ username: string;
+ password: string;
+ email: string;
+ gitAccount: string;
+ admin?: boolean;
+}
diff --git a/src/service/interfaces/common.interfaces.ts b/src/service/interfaces/common.interfaces.ts
new file mode 100644
index 000000000..1e3efbb32
--- /dev/null
+++ b/src/service/interfaces/common.interfaces.ts
@@ -0,0 +1,20 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Generic response carrying a human-readable message. */
+export interface MessageResponse {
+ message: string;
+}
diff --git a/src/service/interfaces/health.interfaces.ts b/src/service/interfaces/health.interfaces.ts
new file mode 100644
index 000000000..e105259fe
--- /dev/null
+++ b/src/service/interfaces/health.interfaces.ts
@@ -0,0 +1,19 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface HealthResponse {
+ message: 'ok';
+}
diff --git a/src/service/interfaces/home.interfaces.ts b/src/service/interfaces/home.interfaces.ts
new file mode 100644
index 000000000..5914707cf
--- /dev/null
+++ b/src/service/interfaces/home.interfaces.ts
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface ApiResources {
+ healthcheck: string;
+ push: string;
+ auth: string;
+}
diff --git a/src/service/routes/home.ts b/src/service/interfaces/push.interfaces.ts
similarity index 67%
rename from src/service/routes/home.ts
rename to src/service/interfaces/push.interfaces.ts
index bfab99c46..94bb90344 100644
--- a/src/service/routes/home.ts
+++ b/src/service/interfaces/push.interfaces.ts
@@ -14,18 +14,15 @@
* limitations under the License.
*/
-import express, { Request, Response } from 'express';
+import { AttestationAnswer } from '../../proxy/processors/types';
-const router = express.Router();
+export interface RejectBody {
+ /** The reason for rejecting the push request. */
+ reason: string;
+}
-const resource = {
- healthcheck: '/api/v1/healthcheck',
- push: '/api/v1/push',
- auth: '/api/auth',
-};
-
-router.get('/', (_req: Request, res: Response) => {
- res.send(resource);
-});
-
-export default router;
+export interface AuthoriseBody {
+ params: {
+ attestation: AttestationAnswer[];
+ };
+}
diff --git a/src/service/routes/config.ts b/src/service/interfaces/repo.interfaces.ts
similarity index 51%
rename from src/service/routes/config.ts
rename to src/service/interfaces/repo.interfaces.ts
index ad61ab422..df2177a9d 100644
--- a/src/service/routes/config.ts
+++ b/src/service/interfaces/repo.interfaces.ts
@@ -14,25 +14,20 @@
* limitations under the License.
*/
-import express, { Request, Response } from 'express';
-import * as config from '../../config';
+import { Repo } from '../../db/types';
-const router = express.Router();
+export interface UsernameBody {
+ username: string;
+}
-router.get('/attestation', (_req: Request, res: Response) => {
- res.send(config.getAttestationConfig());
-});
+export interface CreateRepoBody {
+ url: string;
+ name: string;
+ project: string;
+ users?: { canPush: string[]; canAuthorise: string[] };
+ _id?: string;
+}
-router.get('/urlShortener', (_req: Request, res: Response) => {
- res.send(config.getURLShortener());
-});
-
-router.get('/contactEmail', (_req: Request, res: Response) => {
- res.send(config.getContactEmail());
-});
-
-router.get('/uiRouteAuth', (_req: Request, res: Response) => {
- res.send(config.getUIRouteAuth());
-});
-
-export default router;
+export interface RepoWithProxy extends Repo {
+ proxyURL: string;
+}
diff --git a/src/service/passport/jwtAuthHandler.ts b/src/service/passport/jwtAuthHandler.ts
deleted file mode 100644
index d03fe5555..000000000
--- a/src/service/passport/jwtAuthHandler.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { assignRoles, validateJwt } from './jwtUtils';
-import type { Request, Response, NextFunction } from 'express';
-import { getAPIAuthMethods } from '../../config';
-import {
- AuthenticationElement,
- JwtConfig,
- RoleMapping,
- AuthenticationElementType,
-} from '../../config/generated/config';
-
-export const type = 'jwt';
-
-export const jwtAuthHandler = (overrideConfig: JwtConfig | null = null) => {
- return async (req: Request, res: Response, next: NextFunction): Promise => {
- const apiAuthMethods: AuthenticationElement[] = overrideConfig
- ? [{ type: 'jwt' as AuthenticationElementType, enabled: true, jwtConfig: overrideConfig }]
- : getAPIAuthMethods();
-
- const jwtAuthMethod = apiAuthMethods.find((method) => method.type.toLowerCase() === type);
-
- if (!jwtAuthMethod || !jwtAuthMethod.enabled) {
- return next();
- }
-
- if (req.isAuthenticated && req.isAuthenticated()) {
- return next();
- }
-
- const token = req.header('Authorization');
- if (!token) {
- res.status(401).send('No token provided\n');
- return;
- }
-
- if (!jwtAuthMethod.jwtConfig) {
- res.status(500).send({
- message: 'JWT configuration is missing\n',
- });
- console.log('JWT configuration is missing\n');
- return;
- }
-
- const config = jwtAuthMethod.jwtConfig!;
- const { clientID, authorityURL, expectedAudience, roleMapping } = config;
- const audience = expectedAudience || clientID;
-
- if (!authorityURL) {
- res.status(500).send({
- message: 'OIDC authority URL is not configured\n',
- });
- console.log('OIDC authority URL is not configured\n');
- return;
- }
-
- if (!clientID) {
- res.status(500).send({
- message: 'OIDC client ID is not configured\n',
- });
- console.log('OIDC client ID is not configured\n');
- return;
- }
-
- const tokenParts = token.split(' ');
- const accessToken = tokenParts.length === 2 ? tokenParts[1] : tokenParts[0];
-
- const { verifiedPayload, error } = await validateJwt(
- accessToken,
- authorityURL,
- audience,
- clientID,
- );
-
- if (error || !verifiedPayload) {
- res.status(401).send(error || 'JWT validation failed\n');
- console.log('JWT validation failed\n');
- return;
- }
-
- req.user = verifiedPayload;
- assignRoles(roleMapping as RoleMapping, verifiedPayload, req.user);
-
- console.log('JWT validation successful\n');
- next();
- };
-};
diff --git a/src/service/proxyStore.ts b/src/service/proxyStore.ts
new file mode 100644
index 000000000..f84704072
--- /dev/null
+++ b/src/service/proxyStore.ts
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Proxy } from '../proxy';
+
+let _proxy: Proxy | null = null;
+
+/**
+ * Store the Proxy instance so controllers can access it without constructor injection.
+ * Must be called before RegisterRoutes is invoked in src/service/index.ts.
+ */
+export function setProxy(proxy: Proxy): void {
+ _proxy = proxy;
+}
+
+export function getProxy(): Proxy {
+ if (!_proxy) {
+ throw new Error('Proxy has not been initialised. Call setProxy() before using getProxy().');
+ }
+ return _proxy;
+}
diff --git a/src/service/routes/auth.ts b/src/service/routes/auth.ts
deleted file mode 100644
index a03c80480..000000000
--- a/src/service/routes/auth.ts
+++ /dev/null
@@ -1,282 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express, { Request, Response, NextFunction } from 'express';
-import { getPassport, authStrategies } from '../passport';
-import { getAuthMethods } from '../../config';
-
-import * as db from '../../db';
-import * as passportLocal from '../passport/local';
-import * as passportAD from '../passport/activeDirectory';
-
-import { User } from '../../db/types';
-import { AuthenticationElement } from '../../config/generated/config';
-
-import { isAdminUser, toPublicUser } from './utils';
-import { handleErrorAndLog } from '../../utils/errors';
-
-const router = express.Router();
-const passport = getPassport();
-
-const { GIT_PROXY_UI_HOST: uiHost = 'http://localhost', GIT_PROXY_UI_PORT: uiPort = 3000 } =
- process.env;
-
-router.get('/', (_req: Request, res: Response) => {
- res.status(200).json({
- login: {
- action: 'post',
- uri: '/api/auth/login',
- },
- profile: {
- action: 'get',
- uri: '/api/auth/profile',
- },
- logout: {
- action: 'post',
- uri: '/api/auth/logout',
- },
- });
-});
-
-// login strategies that will work with /login e.g. take username and password
-const appropriateLoginStrategies = [passportLocal.type, passportAD.type];
-// getLoginStrategy fetches the enabled auth methods and identifies if there's an appropriate
-// auth method for username and password login. If there isn't it returns null, if there is it
-// returns the first.
-const getLoginStrategy = () => {
- // returns only enabled auth methods
- // returns at least one enabled auth method
- const enabledAppropriateLoginStrategies = getAuthMethods().filter((am: AuthenticationElement) =>
- appropriateLoginStrategies.includes(am.type.toLowerCase()),
- );
- // for where no login strategies which work for /login are enabled
- // just return null
- if (enabledAppropriateLoginStrategies.length === 0) {
- return null;
- }
- // return the first enabled auth method
- return enabledAppropriateLoginStrategies[0].type.toLowerCase();
-};
-
-const loginSuccessHandler = () => async (req: Request, res: Response) => {
- try {
- const currentUser = toPublicUser({ ...req.user } as User);
- console.log(
- `serivce.routes.auth.login: user logged in, username=${
- currentUser.username
- } profile=${JSON.stringify(currentUser)}`,
- );
- res.send({
- message: 'success',
- user: currentUser,
- });
- } catch (error: unknown) {
- const msg = handleErrorAndLog(error, 'Error logging user in');
- res.status(500).send(`Failed to login: ${msg}`).end();
- }
-};
-
-router.get('/config', (req, res) => {
- const usernamePasswordMethod = getLoginStrategy();
- res.send({
- // enabled username /password auth method
- usernamePasswordMethod: usernamePasswordMethod,
- // other enabled auth methods
- otherMethods: getAuthMethods()
- .map((am) => am.type.toLowerCase())
- .filter((authType) => authType !== usernamePasswordMethod),
- });
-});
-
-// TODO: provide separate auth endpoints for each auth strategy or chain compatibile auth strategies
-// TODO: if providing separate auth methods, inform the frontend so it has relevant UI elements and appropriate client-side behavior
-router.post(
- '/login',
- (req: Request, res: Response, next: NextFunction) => {
- const authType = getLoginStrategy();
- if (authType === null) {
- res.status(403).send('Username and Password based Login is not enabled at this time').end();
- return;
- }
- console.log('going to auth with', authType);
- return passport.authenticate(authType)(req, res, next);
- },
- loginSuccessHandler(),
-);
-
-router.get('/openidconnect', passport.authenticate(authStrategies['openidconnect'].type));
-
-router.get('/openidconnect/callback', (req: Request, res: Response, next: NextFunction) => {
- passport.authenticate(
- authStrategies['openidconnect'].type,
- (err: unknown, user: Partial, info: unknown) => {
- if (err) {
- console.error('Authentication error:', err);
- return res.status(500).end();
- }
- if (!user) {
- console.error('No user found:', info);
- return res.status(401).end();
- }
- req.logIn(user, (err) => {
- if (err) {
- console.error('Login error:', err);
- return res.status(500).end();
- }
- console.log('Logged in successfully. User:', user);
- return res.redirect(`${uiHost}:${uiPort}/dashboard/profile`);
- });
- },
- )(req, res, next);
-});
-
-router.post('/logout', (req: Request, res: Response, next: NextFunction) => {
- req.logout((err: unknown) => {
- if (err) return next(err);
- });
- res.clearCookie('connect.sid');
- res.send({ isAuth: req.isAuthenticated(), user: req.user });
-});
-
-router.get('/profile', async (req: Request, res: Response) => {
- if (!req.user) {
- res
- .status(401)
- .send({
- message: 'Not logged in',
- })
- .end();
- return;
- }
-
- const userVal = await db.findUser((req.user as User).username);
- if (!userVal) {
- res.status(404).send({ message: 'User not found' }).end();
- return;
- }
-
- res.send(toPublicUser(userVal));
-});
-
-router.post('/gitAccount', async (req: Request, res: Response) => {
- if (!req.user) {
- res
- .status(401)
- .send({
- message: 'Not logged in',
- })
- .end();
- return;
- }
-
- try {
- let username =
- req.body.username == null || req.body.username === 'undefined'
- ? req.body.id
- : req.body.username;
- username = username?.split('@')[0];
-
- if (!username) {
- res
- .status(400)
- .send({
- message: 'Missing username. Git account not updated',
- })
- .end();
- return;
- }
-
- const reqUser = await db.findUser((req.user as User).username);
- if (username !== reqUser?.username && !reqUser?.admin) {
- res
- .status(403)
- .send({
- message: 'Must be an admin to update a different account',
- })
- .end();
- return;
- }
-
- const user = await db.findUser(username);
- if (!user) {
- res
- .status(404)
- .send({
- message: 'User not found',
- })
- .end();
- return;
- }
-
- user.gitAccount = req.body.gitAccount;
- db.updateUser(user);
- res.status(200).end();
- } catch (error: unknown) {
- const msg = handleErrorAndLog(error, 'Failed to update git account');
- res
- .status(500)
- .send({
- message: msg,
- })
- .end();
- }
-});
-
-router.post('/create-user', async (req: Request, res: Response) => {
- if (!isAdminUser(req.user)) {
- res
- .status(403)
- .send({
- message: 'Not authorized to create users',
- })
- .end();
- return;
- }
-
- try {
- const { username, password, email, gitAccount, admin: isAdmin = false } = req.body;
-
- if (!username || !password || !email || !gitAccount) {
- res
- .status(400)
- .send({
- message:
- 'Missing required fields: username, password, email, and gitAccount are required',
- })
- .end();
- return;
- }
-
- await db.createUser(username, password, email, gitAccount, isAdmin);
- res
- .status(201)
- .send({
- message: 'User created successfully',
- username,
- })
- .end();
- } catch (error: unknown) {
- const msg = handleErrorAndLog(error, 'Failed to create user');
- res
- .status(500)
- .send({
- message: msg,
- })
- .end();
- }
-});
-
-export default { router, loginSuccessHandler };
diff --git a/src/service/routes/index.ts b/src/service/routes/index.ts
deleted file mode 100644
index 80d6c315d..000000000
--- a/src/service/routes/index.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express from 'express';
-import auth from './auth';
-import push from './push';
-import home from './home';
-import repo from './repo';
-import users from './users';
-import healthcheck from './healthcheck';
-import config from './config';
-import { jwtAuthHandler } from '../passport/jwtAuthHandler';
-import { Proxy } from '../../proxy';
-
-const routes = (proxy: Proxy) => {
- const router = express.Router();
- router.use('/api', home);
- router.use('/api/auth', auth.router);
- router.use('/api/v1/healthcheck', healthcheck);
- router.use('/api/v1/push', jwtAuthHandler(), push);
- router.use('/api/v1/repo', jwtAuthHandler(), repo(proxy));
- router.use('/api/v1/user', jwtAuthHandler(), users);
- router.use('/api/v1/config', config);
- return router;
-};
-
-export default routes;
diff --git a/src/service/routes/push.ts b/src/service/routes/push.ts
deleted file mode 100644
index 1e900d6ea..000000000
--- a/src/service/routes/push.ts
+++ /dev/null
@@ -1,278 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express, { Request, Response } from 'express';
-import * as db from '../../db';
-import { PushQuery } from '../../db/types';
-import { AttestationConfig } from '../../config/generated/config';
-import { getAttestationConfig } from '../../config';
-import { AttestationAnswer, Rejection } from '../../proxy/processors/types';
-
-interface AuthoriseRequest {
- params: {
- attestation: AttestationAnswer[];
- };
-}
-
-const router = express.Router();
-
-router.get('/', async (req: Request, res: Response) => {
- const query: Partial = {
- type: 'push',
- };
-
- for (const key in req.query) {
- if (!key) continue;
- if (key === 'limit' || key === 'skip') continue;
-
- const rawValue = req.query[key];
- let parsedValue: boolean | undefined;
- if (rawValue === 'false') parsedValue = false;
- if (rawValue === 'true') parsedValue = true;
- query[key] = parsedValue ?? rawValue?.toString();
- }
-
- res.send(await db.getPushes(query));
-});
-
-router.get('/:id', async (req: Request<{ id: string }>, res: Response) => {
- const id = req.params.id;
- const push = await db.getPush(id);
- if (push) {
- res.send(push);
- } else {
- res.status(404).send({
- message: 'not found',
- });
- }
-});
-
-router.post('/:id/reject', async (req: Request<{ id: string }>, res: Response) => {
- if (!req.user) {
- res.status(401).send({
- message: 'Not logged in',
- });
- return;
- }
-
- const id = req.params.id;
- const { username } = req.user as { username: string };
- const { reason } = req.body;
-
- if (!reason || !reason.trim()) {
- res.status(400).send({
- message: 'Rejection reason is required',
- });
- return;
- }
-
- // Get the push request
- const push = await getValidPushOrRespond(id, res);
- if (!push) return;
-
- // Get the committer of the push via their email
- const committerEmail = push.userEmail;
- const list = await db.getUsers({ email: committerEmail });
-
- if (list.length === 0) {
- res.status(404).send({
- message: `No user found with the committer's email address: ${committerEmail}`,
- });
- return;
- }
-
- if (list[0].username.toLowerCase() === username.toLowerCase() && !list[0].admin) {
- res.status(403).send({
- message: `Cannot reject your own changes`,
- });
- return;
- }
-
- const isAllowed = await db.canUserApproveRejectPush(id, username);
-
- if (isAllowed) {
- const reviewerList = await db.getUsers({ username });
- const reviewerEmail = reviewerList[0].email;
-
- if (!reviewerEmail) {
- res.status(404).send({
- message: `There was no registered email address for the reviewer: ${username}`,
- });
- return;
- }
-
- const rejection: Rejection = {
- reason,
- timestamp: new Date(),
- reviewer: {
- username,
- email: reviewerEmail,
- },
- };
-
- const result = await db.reject(id, rejection);
- console.log(
- `User ${username} rejected push request for ${id}${reason ? ` with reason: ${reason}` : ''}`,
- );
- res.send(result);
- } else {
- res.status(403).send({
- message: `User ${username} is not authorised to reject changes on this project`,
- });
- }
-});
-
-router.post(
- '/:id/authorise',
- async (req: Request<{ id: string }, unknown, AuthoriseRequest>, res: Response) => {
- if (!req.user) {
- res.status(401).send({
- message: 'Not logged in',
- });
- return;
- }
-
- const answers = req.body.params?.attestation;
-
- const attestationComplete = validateAttestation(answers, getAttestationConfig());
-
- if (!attestationComplete) {
- res.status(400).send({
- message: 'Attestation is not complete',
- });
- return;
- }
-
- const id = req.params.id;
-
- const { username } = req.user as { username: string };
-
- const push = await db.getPush(id);
- if (!push) {
- res.status(404).send({
- message: 'Push request not found',
- });
- return;
- }
-
- // Get the committer of the push via their email address
- const committerEmail = push.userEmail;
-
- const list = await db.getUsers({ email: committerEmail });
-
- if (list.length === 0) {
- res.status(404).send({
- message: `No user found with the committer's email address: ${committerEmail}`,
- });
- return;
- }
-
- if (list[0].username.toLowerCase() === username.toLowerCase() && !list[0].admin) {
- res.status(403).send({
- message: `Cannot approve your own changes`,
- });
- return;
- }
-
- // If we are not the author, now check that we are allowed to authorise on this
- // repo
- const isAllowed = await db.canUserApproveRejectPush(id, username);
- if (isAllowed) {
- console.log(`User ${username} approved push request for ${id}`);
-
- const reviewerList = await db.getUsers({ username });
- const reviewerEmail = reviewerList[0].email;
-
- if (!reviewerEmail) {
- res.status(404).send({
- message: `There was no registered email address for the reviewer: ${username}`,
- });
- return;
- }
-
- const attestation = {
- answers,
- timestamp: new Date(),
- reviewer: {
- username,
- email: reviewerEmail,
- },
- };
- const result = await db.authorise(id, attestation);
- res.send(result);
- } else {
- res.status(403).send({
- message: `User ${username} not authorised to approve pushes on this project`,
- });
- }
- },
-);
-
-router.post('/:id/cancel', async (req: Request<{ id: string }>, res: Response) => {
- if (!req.user) {
- res.status(401).send({
- message: 'Not logged in',
- });
- return;
- }
-
- const id = req.params.id;
- const { username } = req.user as { username: string };
-
- const isAllowed = await db.canUserCancelPush(id, username);
-
- if (isAllowed) {
- const result = await db.cancel(id);
- console.log(`User ${username} canceled push request for ${id}`);
- res.send(result);
- } else {
- console.log(`User ${username} not authorised to cancel push request for ${id}`);
- res.status(403).send({
- message: `User ${username} not authorised to cancel push requests on this project`,
- });
- }
-});
-
-async function getValidPushOrRespond(id: string, res: Response) {
- console.log('getValidPushOrRespond', { id });
- const push = await db.getPush(id);
-
- if (!push) {
- res.status(404).send({ message: `Push request not found` });
- return null;
- }
-
- if (!push.userEmail) {
- res.status(400).send({ message: `Push request has no user email` });
- return null;
- }
-
- return push;
-}
-
-function validateAttestation(answers: AttestationAnswer[], config: AttestationConfig): boolean {
- const configQuestions = config.questions ?? [];
-
- if (answers.length !== configQuestions.length) {
- return false;
- }
-
- const configLabels = new Set(configQuestions.map((q) => q.label));
-
- return answers.every((answer) => configLabels.has(answer.label) && !!answer.checked);
-}
-
-export default router;
diff --git a/src/service/routes/repo.ts b/src/service/routes/repo.ts
deleted file mode 100644
index 7e259d0aa..000000000
--- a/src/service/routes/repo.ts
+++ /dev/null
@@ -1,231 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express, { Request, Response } from 'express';
-
-import * as db from '../../db';
-import { getProxyURL } from '../urls';
-import { getAllProxiedHosts } from '../../db';
-import { RepoQuery } from '../../db/types';
-import { isAdminUser } from './utils';
-import { Proxy } from '../../proxy';
-import { handleErrorAndLog } from '../../utils/errors';
-
-function repo(proxy: Proxy) {
- const router = express.Router();
-
- router.get('/', async (req: Request, res: Response) => {
- const proxyURL = getProxyURL(req);
- const query: Partial = {};
-
- for (const key in req.query) {
- if (!key) continue;
- if (key === 'limit' || key === 'skip') continue;
-
- const rawValue = req.query[key];
- let parsedValue: boolean | undefined;
- if (rawValue === 'false') parsedValue = false;
- if (rawValue === 'true') parsedValue = true;
- query[key] = parsedValue ?? rawValue?.toString();
- }
-
- const qd = await db.getRepos(query);
- res.send(qd.map((d) => ({ ...d, proxyURL })));
- });
-
- router.get('/:id', async (req: Request<{ id: string }>, res: Response) => {
- const proxyURL = getProxyURL(req);
- const _id = req.params.id;
- const qd = await db.getRepoById(_id);
- res.send({ ...qd, proxyURL });
- });
-
- router.patch('/:id/user/push', async (req: Request<{ id: string }>, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- const _id = req.params.id;
- const username = req.body.username.toLowerCase();
- const user = await db.findUser(username);
-
- if (!user) {
- res.status(400).send({ error: 'User does not exist' });
- return;
- }
-
- await db.addUserCanPush(_id, username);
- res.send({ message: 'created' });
- });
-
- router.patch('/:id/user/authorise', async (req: Request<{ id: string }>, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- const _id = req.params.id;
- const username = req.body.username;
- const user = await db.findUser(username);
-
- if (!user) {
- res.status(400).send({ error: 'User does not exist' });
- return;
- }
-
- await db.addUserCanAuthorise(_id, username);
- res.send({ message: 'created' });
- });
-
- router.delete(
- '/:id/user/authorise/:username',
- async (req: Request<{ id: string; username: string }>, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- const _id = req.params.id;
- const username = req.params.username;
- const user = await db.findUser(username);
-
- if (!user) {
- res.status(400).send({ error: 'User does not exist' });
- return;
- }
-
- await db.removeUserCanAuthorise(_id, username);
- res.send({ message: 'created' });
- },
- );
-
- router.delete(
- '/:id/user/push/:username',
- async (req: Request<{ id: string; username: string }>, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- const _id = req.params.id;
- const username = req.params.username;
- const user = await db.findUser(username);
-
- if (!user) {
- res.status(400).send({ error: 'User does not exist' });
- return;
- }
-
- await db.removeUserCanPush(_id, username);
- res.send({ message: 'created' });
- },
- );
-
- router.delete('/:id/delete', async (req: Request<{ id: string }>, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- const _id = req.params.id;
-
- // determine if we need to restart the proxy
- const previousHosts = await getAllProxiedHosts();
- await db.deleteRepo(_id);
- const currentHosts = await getAllProxiedHosts();
-
- if (currentHosts.length < previousHosts.length) {
- // restart the proxy
- console.log('Restarting the proxy to remove a host');
- await proxy.stop();
- await proxy.start();
- }
-
- res.send({ message: 'deleted' });
- });
-
- router.post('/', async (req: Request, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- if (!req.body.url) {
- res.status(400).send({
- message: 'Repository url is required',
- });
- return;
- }
-
- const repo = await db.getRepoByUrl(req.body.url);
- if (repo) {
- res.status(409).send({
- message: `Repository ${req.body.url} already exists!`,
- });
- } else {
- try {
- // figure out if this represent a new domain to proxy
- let newOrigin = true;
-
- const existingHosts = await getAllProxiedHosts();
- existingHosts.forEach((h) => {
- // assume SSL is in use and that our origins are missing the protocol
- if (req.body.url.startsWith(`https://${h}`)) {
- newOrigin = false;
- }
- });
-
- console.log(
- `API request to proxy repository ${req.body.url} is for a new origin: ${newOrigin},\n\texisting origin list was: ${JSON.stringify(existingHosts)}`,
- );
-
- // create the repository
- const repoDetails = await db.createRepo(req.body);
- const proxyURL = getProxyURL(req);
-
- // restart the proxy if we're proxying a new domain
- if (newOrigin) {
- console.log('Restarting the proxy to handle an additional host');
- await proxy.stop();
- await proxy.start();
- }
-
- // return data on the new repository (including it's _id and the proxyUrl)
- res.send({ ...repoDetails, proxyURL, message: 'created' });
- } catch (error: unknown) {
- const msg = handleErrorAndLog(error, 'Repository creation failed');
- res.status(500).send({ message: msg });
- }
- }
- });
-
- return router;
-}
-
-export default repo;
diff --git a/src/service/routes/users.ts b/src/service/routes/users.ts
deleted file mode 100644
index 7a20307e9..000000000
--- a/src/service/routes/users.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express, { Request, Response } from 'express';
-const router = express.Router();
-
-import * as db from '../../db';
-import { toPublicUser } from './utils';
-
-router.get('/', async (req: Request, res: Response) => {
- console.log('fetching users');
- const users = await db.getUsers();
- res.send(users.map(toPublicUser));
-});
-
-router.get('/:id', async (req: Request<{ id: string }>, res: Response) => {
- const username = req.params.id.toLowerCase();
- console.log(`Retrieving details for user: ${username}`);
- const user = await db.findUser(username);
- if (!user) {
- res
- .status(404)
- .send({
- message: `User ${username} not found`,
- })
- .end();
- return;
- }
- res.send(toPublicUser(user));
-});
-
-export default router;
diff --git a/test/service.tls.test.ts b/test/service.tls.test.ts
index dd0e163f9..5ef8bff43 100644
--- a/test/service.tls.test.ts
+++ b/test/service.tls.test.ts
@@ -87,6 +87,14 @@ describe('Service Module TLS', () => {
initialize: vi.fn().mockReturnValue((_req: any, _res: any, next: any) => next()),
session: vi.fn().mockReturnValue((_req: any, _res: any, next: any) => next()),
}),
+ getPassport: vi.fn().mockReturnValue({
+ authenticate: vi.fn().mockReturnValue((_req: any, _res: any, next: any) => next()),
+ }),
+ authStrategies: {
+ local: { type: 'local' },
+ activedirectory: { type: 'activedirectory' },
+ openidconnect: { type: 'openidconnect' },
+ },
}));
vi.doMock('../src/service/routes', () => ({
diff --git a/test/services/routes/auth.test.ts b/test/services/routes/auth.test.ts
index 4008cf9ed..34597919e 100644
--- a/test/services/routes/auth.test.ts
+++ b/test/services/routes/auth.test.ts
@@ -16,22 +16,51 @@
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
import request from 'supertest';
-import express, { Express, Request, Response } from 'express';
-import authRoutes from '../../../src/service/routes/auth';
+import express, { Express } from 'express';
+import { RegisterRoutes } from '../../../src/service/generatedRoutes';
import * as db from '../../../src/db';
+import { ValidateError } from 'tsoa';
-const newApp = (username?: string): Express => {
+/**
+ * Builds a minimal Express app with tsoa routes registered.
+ * When `username` is supplied the request is pre-authenticated via middleware,
+ * simulating a session-authenticated user.
+ */
+const newApp = (username?: string, isAdmin = false): Express => {
const app = express();
app.use(express.json());
if (username) {
app.use((req, _res, next) => {
- req.user = { username };
+ req.user = { username, admin: isAdmin };
+ next();
+ });
+ } else {
+ app.use((_req, _res, next) => {
next();
});
}
- app.use('/auth', authRoutes.router);
+ // Generic error handler so tsoa thrown errors propagate as HTTP responses.
+ RegisterRoutes(app);
+
+ // tsoa validation errors
+ app.use((err: any, _req: any, res: any, next: any) => {
+ if (err instanceof ValidateError) {
+ return res.status(400).json({
+ message: 'Validation failed',
+ details: err.fields,
+ });
+ }
+ next(err);
+ });
+
+ // Generic error handler so tsoa thrown errors propagate as HTTP responses.
+ app.use((err: any, _req: any, res: any, next: any) => {
+ if (res.headersSent) return next(err);
+ res.status(err.status ?? 500).json({ message: err.message });
+ });
+
return app;
};
@@ -40,7 +69,7 @@ describe('Auth API', () => {
vi.restoreAllMocks();
});
- describe('POST /gitAccount', () => {
+ describe('POST /api/auth/gitAccount', () => {
beforeEach(() => {
vi.spyOn(db, 'findUser').mockImplementation((username: string) => {
if (username === 'alice') {
@@ -73,7 +102,7 @@ describe('Auth API', () => {
});
it('should return 401 Unauthorized if authenticated user not in request', async () => {
- const res = await request(newApp()).post('/auth/gitAccount').send({
+ const res = await request(newApp()).post('/api/auth/gitAccount').send({
username: 'alice',
gitAccount: '',
});
@@ -82,7 +111,7 @@ describe('Auth API', () => {
});
it('should return 400 Bad Request if username is missing', async () => {
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -90,7 +119,7 @@ describe('Auth API', () => {
});
it('should return 400 Bad Request if username is undefined', async () => {
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: undefined,
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -99,7 +128,7 @@ describe('Auth API', () => {
});
it('should return 400 Bad Request if username is null', async () => {
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: null,
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -108,7 +137,7 @@ describe('Auth API', () => {
});
it('should return 400 Bad Request if username is an empty string', async () => {
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: '',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -117,7 +146,7 @@ describe('Auth API', () => {
});
it('should return 403 Forbidden if user is not an admin', async () => {
- const res = await request(newApp('bob')).post('/auth/gitAccount').send({
+ const res = await request(newApp('bob')).post('/api/auth/gitAccount').send({
username: 'alice',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -126,7 +155,7 @@ describe('Auth API', () => {
});
it('should return 404 Not Found if user is not found', async () => {
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: 'non-existent-user',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -137,7 +166,7 @@ describe('Auth API', () => {
it('should return 200 OK if user is an admin and updates git account for authenticated user', async () => {
const updateUserSpy = vi.spyOn(db, 'updateUser').mockResolvedValue();
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: 'alice',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -158,7 +187,7 @@ describe('Auth API', () => {
it("should prevent non-admin users from changing a different user's gitAccount", async () => {
const updateUserSpy = vi.spyOn(db, 'updateUser').mockResolvedValue();
- const res = await request(newApp('bob')).post('/auth/gitAccount').send({
+ const res = await request(newApp('bob')).post('/api/auth/gitAccount').send({
username: 'phil',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -170,7 +199,7 @@ describe('Auth API', () => {
it("should allow admin users to change a different user's gitAccount", async () => {
const updateUserSpy = vi.spyOn(db, 'updateUser').mockResolvedValue();
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: 'bob',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -191,7 +220,7 @@ describe('Auth API', () => {
it('should allow non-admin users to update their own gitAccount', async () => {
const updateUserSpy = vi.spyOn(db, 'updateUser').mockResolvedValue();
- const res = await request(newApp('bob')).post('/auth/gitAccount').send({
+ const res = await request(newApp('bob')).post('/api/auth/gitAccount').send({
username: 'bob',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -210,46 +239,9 @@ describe('Auth API', () => {
});
});
- describe('loginSuccessHandler', () => {
- it('should log in user and return public user data', async () => {
- const user = {
- username: 'bob',
- password: 'secret',
- email: 'bob@example.com',
- displayName: 'Bob',
- admin: false,
- gitAccount: '',
- title: '',
- };
-
- const sendSpy = vi.fn();
- const res = {
- send: sendSpy,
- };
-
- await authRoutes.loginSuccessHandler()(
- { user } as unknown as Request,
- res as unknown as Response,
- );
-
- expect(sendSpy).toHaveBeenCalledOnce();
- expect(sendSpy).toHaveBeenCalledWith({
- message: 'success',
- user: {
- admin: false,
- displayName: 'Bob',
- email: 'bob@example.com',
- gitAccount: '',
- title: '',
- username: 'bob',
- },
- });
- });
- });
-
- describe('GET /profile', () => {
+ describe('GET /api/auth/profile', () => {
it('should return 401 Unauthorized if user is not logged in', async () => {
- const res = await request(newApp()).get('/auth/profile');
+ const res = await request(newApp()).get('/api/auth/profile');
expect(res.status).toBe(401);
});
@@ -265,7 +257,7 @@ describe('Auth API', () => {
title: '',
});
- const res = await request(newApp('alice')).get('/auth/profile');
+ const res = await request(newApp('alice')).get('/api/auth/profile');
expect(res.status).toBe(200);
expect(res.body).toEqual({
username: 'alice',
@@ -278,15 +270,17 @@ describe('Auth API', () => {
});
it('should return 404 Not Found if user is not found', async () => {
- const res = await request(newApp('non-existent-user')).get('/auth/profile');
+ vi.spyOn(db, 'findUser').mockResolvedValue(null);
+
+ const res = await request(newApp('non-existent-user')).get('/api/auth/profile');
expect(res.status).toBe(404);
expect(res.body).toEqual({ message: 'User not found' });
});
});
- describe('GET /', () => {
+ describe('GET /api/auth', () => {
it('should return 200 OK and the auth endpoints', async () => {
- const res = await request(newApp()).get('/auth');
+ const res = await request(newApp()).get('/api/auth');
expect(res.status).toBe(200);
expect(res.body).toEqual({
login: {
@@ -305,9 +299,9 @@ describe('Auth API', () => {
});
});
- describe('GET /config', () => {
+ describe('GET /api/auth/config', () => {
it('should return 200 OK and the default auth config', async () => {
- const res = await request(newApp()).get('/auth/config');
+ const res = await request(newApp()).get('/api/auth/config');
expect(res.status).toBe(200);
expect(res.body).toEqual({
usernamePasswordMethod: 'local',
diff --git a/test/services/routes/config.test.ts b/test/services/routes/config.test.ts
index 4caa53758..536e3d7ce 100644
--- a/test/services/routes/config.test.ts
+++ b/test/services/routes/config.test.ts
@@ -17,7 +17,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import express, { Express } from 'express';
import request from 'supertest';
-import configRouter from '../../../src/service/routes/config';
+import { RegisterRoutes } from '../../../src/service/generatedRoutes';
import * as config from '../../../src/config';
describe('Config API', () => {
@@ -26,7 +26,7 @@ describe('Config API', () => {
beforeEach(() => {
app = express();
app.use(express.json());
- app.use('/config', configRouter);
+ RegisterRoutes(app);
vi.spyOn(config, 'getAttestationConfig').mockReturnValue({ questions: [] });
vi.spyOn(config, 'getURLShortener').mockReturnValue('https://url-shortener.com');
@@ -38,26 +38,27 @@ describe('Config API', () => {
vi.restoreAllMocks();
});
- it('GET /config/attestation should return 200 OK and the default attestation config', async () => {
- const res = await request(app).get('/config/attestation');
+ it('GET /api/v1/config/attestation should return 200 OK and the default attestation config', async () => {
+ const res = await request(app).get('/api/v1/config/attestation');
expect(res.status).toBe(200);
expect(res.body).toEqual({ questions: [] });
});
- it('GET /config/urlShortener should return 200 OK and the default url shortener config', async () => {
- const res = await request(app).get('/config/urlShortener');
+ it('GET /api/v1/config/urlShortener should return 200 OK and the url shortener config', async () => {
+ const res = await request(app).get('/api/v1/config/urlShortener');
expect(res.status).toBe(200);
- expect(res.text).toBe('https://url-shortener.com'); // Check res.text as it gets serialized as a string
+ // tsoa sends plain strings via res.send() so the value is in res.text, not res.body.
+ expect(res.text).toBe('https://url-shortener.com');
});
- it('GET /config/contactEmail should return 200 OK and the default contact email', async () => {
- const res = await request(app).get('/config/contactEmail');
+ it('GET /api/v1/config/contactEmail should return 200 OK and the contact email', async () => {
+ const res = await request(app).get('/api/v1/config/contactEmail');
expect(res.status).toBe(200);
expect(res.text).toBe('test@example.com');
});
- it('GET /config/uiRouteAuth should return 200 OK and the default ui route auth config', async () => {
- const res = await request(app).get('/config/uiRouteAuth');
+ it('GET /api/v1/config/uiRouteAuth should return 200 OK and the ui route auth config', async () => {
+ const res = await request(app).get('/api/v1/config/uiRouteAuth');
expect(res.status).toBe(200);
expect(res.body).toEqual({ enabled: false, rules: [] });
});
diff --git a/test/services/routes/healthCheck.test.ts b/test/services/routes/healthCheck.test.ts
index 122203210..c1e6c02c6 100644
--- a/test/services/routes/healthCheck.test.ts
+++ b/test/services/routes/healthCheck.test.ts
@@ -17,7 +17,7 @@
import { describe, it, expect, beforeEach } from 'vitest';
import express, { Express } from 'express';
import request from 'supertest';
-import healthcheck from '../../../src/service/routes/healthcheck';
+import { RegisterRoutes } from '../../../src/service/generatedRoutes';
describe('Health Check API', () => {
let app: Express;
@@ -25,11 +25,11 @@ describe('Health Check API', () => {
beforeEach(() => {
app = express();
app.use(express.json());
- app.use('/healthCheck', healthcheck);
+ RegisterRoutes(app);
});
- it('GET /healthCheck should return 200 OK and the health check message', async () => {
- const res = await request(app).get('/healthCheck');
+ it('GET /api/v1/healthcheck should return 200 OK and the health check message', async () => {
+ const res = await request(app).get('/api/v1/healthcheck');
expect(res.status).toBe(200);
expect(res.body).toEqual({ message: 'ok' });
});
diff --git a/test/services/routes/users.test.ts b/test/services/routes/users.test.ts
index 41ec52f58..c326e67e8 100644
--- a/test/services/routes/users.test.ts
+++ b/test/services/routes/users.test.ts
@@ -17,7 +17,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import express, { Express } from 'express';
import request from 'supertest';
-import usersRouter from '../../../src/service/routes/users';
+import { RegisterRoutes } from '../../../src/service/generatedRoutes';
import * as db from '../../../src/db';
describe('Users API', () => {
@@ -26,7 +26,15 @@ describe('Users API', () => {
beforeEach(() => {
app = express();
app.use(express.json());
- app.use('/users', usersRouter);
+ app.use((req, _res, next) => {
+ req.user = { username: 'testuser' };
+ next();
+ });
+ RegisterRoutes(app);
+ app.use((err: any, _req: any, res: any, next: any) => {
+ if (res.headersSent) return next(err);
+ res.status(err.status ?? 500).json({ message: err.message });
+ });
vi.spyOn(db, 'getUsers').mockResolvedValue([
{
@@ -53,8 +61,8 @@ describe('Users API', () => {
vi.restoreAllMocks();
});
- it('GET /users only serializes public data needed for ui, not user secrets like password', async () => {
- const res = await request(app).get('/users');
+ it('GET /api/v1/user only serializes public data needed for ui, not user secrets like password', async () => {
+ const res = await request(app).get('/api/v1/user');
expect(res.status).toBe(200);
expect(res.body).toEqual([
@@ -69,8 +77,8 @@ describe('Users API', () => {
]);
});
- it('GET /users/:id does not serialize password', async () => {
- const res = await request(app).get('/users/bob');
+ it('GET /api/v1/user/:id does not serialize password', async () => {
+ const res = await request(app).get('/api/v1/user/bob');
expect(res.status).toBe(200);
console.log(`Response body: ${JSON.stringify(res.body)}`);
@@ -84,10 +92,10 @@ describe('Users API', () => {
});
});
- it('GET /users/:id should return 404 Not Found if user is not found', async () => {
+ it('GET /api/v1/user/:id should return 404 Not Found if user is not found', async () => {
vi.restoreAllMocks();
- const res = await request(app).get('/users/non-existent');
+ const res = await request(app).get('/api/v1/user/non-existent');
expect(res.status).toBe(404);
expect(res.body).toEqual({ message: 'User non-existent not found' });
});
diff --git a/test/testJwtAuthHandler.test.ts b/test/testJwtAuthHandler.test.ts
index 7d41872e1..b2d14fade 100644
--- a/test/testJwtAuthHandler.test.ts
+++ b/test/testJwtAuthHandler.test.ts
@@ -16,12 +16,12 @@
import axios from 'axios';
import crypto from 'crypto';
-import { NextFunction } from 'express';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { describe, it, expect, vi, beforeEach, afterEach, MockInstance } from 'vitest';
import { assignRoles, getJwks, validateJwt } from '../src/service/passport/jwtUtils';
-import { jwtAuthHandler } from '../src/service/passport/jwtAuthHandler';
+import { expressAuthentication } from '../src/service/authentication';
+import * as configModule from '../src/config';
import { JwtConfig, RoleMapping } from '../src/config/generated/config';
function generateRsaKeyPair() {
@@ -189,17 +189,13 @@ describe('JWT', () => {
});
});
- describe('jwtAuthHandler', () => {
+ describe('expressAuthentication', () => {
let req: any;
- let res: any;
- let next: NextFunction;
let jwtConfig: JwtConfig;
- let validVerifyResponse: JwtPayload;
+ let mockGetAPIAuthMethods: MockInstance;
beforeEach(() => {
req = { header: vi.fn(), isAuthenticated: vi.fn(), user: {} };
- res = { status: vi.fn().mockReturnThis(), send: vi.fn() };
- next = vi.fn();
jwtConfig = {
clientID: 'client-id',
@@ -208,62 +204,57 @@ describe('JWT', () => {
roleMapping: { admin: { admin: 'admin' } },
};
- validVerifyResponse = {
- header: { kid: '123' },
- azp: 'client-id',
- sub: 'user123',
- admin: 'admin',
- };
+ mockGetAPIAuthMethods = vi
+ .spyOn(configModule, 'getAPIAuthMethods')
+ .mockReturnValue([{ type: 'jwt', enabled: true, jwtConfig }] as any);
});
afterEach(() => vi.restoreAllMocks());
- it('should call next if user is authenticated', async () => {
+ it('should return user if already authenticated via session', async () => {
req.isAuthenticated.mockReturnValue(true);
- await jwtAuthHandler()(req, res, next);
- expect(next).toHaveBeenCalledOnce();
+ const result = await expressAuthentication(req, 'jwt');
+ expect(result).toBe(req.user);
});
- it('should return 401 if no token provided', async () => {
- req.header.mockReturnValue(null);
- await jwtAuthHandler(jwtConfig)(req, res, next);
+ it('should return undefined if JWT auth method is not configured', async () => {
+ mockGetAPIAuthMethods.mockReturnValue([]);
+ req.isAuthenticated.mockReturnValue(false);
+ const result = await expressAuthentication(req, 'jwt');
+ expect(result).toBeUndefined();
+ });
- expect(res.status).toHaveBeenCalledWith(401);
- expect(res.send).toHaveBeenCalledWith('No token provided\n');
+ it('should throw 401 if no token provided', async () => {
+ req.isAuthenticated.mockReturnValue(false);
+ req.header.mockReturnValue(null);
+ await expect(expressAuthentication(req, 'jwt')).rejects.toMatchObject({ status: 401 });
});
- it('should return 500 if authorityURL not configured', async () => {
+ it('should throw 500 if authorityURL not configured', async () => {
+ req.isAuthenticated.mockReturnValue(false);
req.header.mockReturnValue('Bearer fake-token');
jwtConfig.authorityURL = null;
- vi.spyOn(jwt, 'verify').mockReturnValue(validVerifyResponse);
-
- await jwtAuthHandler(jwtConfig)(req, res, next);
-
- expect(res.status).toHaveBeenCalledWith(500);
- expect(res.send).toHaveBeenCalledWith({ message: 'OIDC authority URL is not configured\n' });
+ await expect(expressAuthentication(req, 'jwt')).rejects.toMatchObject({ status: 500 });
});
- it('should return 500 if clientID not configured', async () => {
+ it('should throw 500 if clientID not configured', async () => {
+ req.isAuthenticated.mockReturnValue(false);
req.header.mockReturnValue('Bearer fake-token');
jwtConfig.clientID = null;
- vi.spyOn(jwt, 'verify').mockReturnValue(validVerifyResponse);
-
- await jwtAuthHandler(jwtConfig)(req, res, next);
-
- expect(res.status).toHaveBeenCalledWith(500);
- expect(res.send).toHaveBeenCalledWith({ message: 'OIDC client ID is not configured\n' });
+ await expect(expressAuthentication(req, 'jwt')).rejects.toMatchObject({ status: 500 });
});
- it('should return 401 if JWT validation fails', async () => {
+ it('should throw 401 if JWT validation fails', async () => {
+ req.isAuthenticated.mockReturnValue(false);
req.header.mockReturnValue('Bearer fake-token');
vi.spyOn(jwt, 'verify').mockImplementation(() => {
throw new Error('Invalid token');
});
+ await expect(expressAuthentication(req, 'jwt')).rejects.toMatchObject({ status: 401 });
+ });
- await jwtAuthHandler(jwtConfig)(req, res, next);
-
- expect(res.status).toHaveBeenCalledWith(401);
- expect(res.send).toHaveBeenCalledWith(expect.stringMatching(/Invalid JWT:/));
+ it('should throw 401 for unknown security scheme', async () => {
+ await expect(expressAuthentication(req, 'unknown')).rejects.toMatchObject({ status: 401 });
});
});
});
diff --git a/test/testLogin.test.ts b/test/testLogin.test.ts
index 07ec1d2ba..472a253aa 100644
--- a/test/testLogin.test.ts
+++ b/test/testLogin.test.ts
@@ -197,9 +197,10 @@ describe('login', () => {
});
expect(res.status).toBe(400);
- expect(res.body.message).toBe(
- 'Missing required fields: username, password, email, and gitAccount are required',
- );
+ // tsoa validates required body fields before the controller runs.
+ // The 'password' field is missing; tsoa prefixes body field errors with 'body.'.
+ expect(res.body.message).toBe('Validation failed');
+ expect(res.body.details).toHaveProperty('body.password');
});
it('should successfully create a new user', async () => {
diff --git a/test/testProxyRoute.test.ts b/test/testProxyRoute.test.ts
index 32f720015..63360e78f 100644
--- a/test/testProxyRoute.test.ts
+++ b/test/testProxyRoute.test.ts
@@ -58,6 +58,14 @@ const TEST_UNKNOWN_REPO = {
fallbackUrlPrefix: '/finos/fdc3.git',
};
+// `host` / `proxyUrlPrefix` / `fallbackUrlPrefix` are test-only helpers; the
+// API rejects them as unknown properties on CreateRepoBody.
+const toCreateBody = ({ url, name, project }: { url: string; name: string; project: string }) => ({
+ url,
+ name,
+ project,
+});
+
afterAll(() => {
vi.resetModules();
});
@@ -105,7 +113,7 @@ describe('proxy express application', () => {
const res2 = await request(apiApp)
.post('/api/v1/repo')
.set('Cookie', cookie)
- .send(TEST_DEFAULT_REPO);
+ .send(toCreateBody(TEST_DEFAULT_REPO));
expect(res2.status).toBe(200);
}
});
@@ -122,7 +130,10 @@ describe('proxy express application', () => {
// Ensure default repo exists
const repo = await db.getRepoByUrl(TEST_DEFAULT_REPO.url);
if (!repo) {
- await request(apiApp).post('/api/v1/repo').set('Cookie', cookie).send(TEST_DEFAULT_REPO);
+ await request(apiApp)
+ .post('/api/v1/repo')
+ .set('Cookie', cookie)
+ .send(toCreateBody(TEST_DEFAULT_REPO));
}
// proxy a fetch request
@@ -160,7 +171,7 @@ describe('proxy express application', () => {
const res = await request(apiApp)
.post('/api/v1/repo')
.set('Cookie', cookie)
- .send(TEST_GITLAB_REPO);
+ .send(toCreateBody(TEST_GITLAB_REPO));
expect(res.status).toBe(200);
// confirm that the repo was created in the DB
@@ -188,7 +199,10 @@ describe('proxy express application', () => {
// Ensure the gitlab test repo exists (create it if a previous test didn't)
let repo = await db.getRepoByUrl(TEST_GITLAB_REPO.url);
if (!repo) {
- await request(apiApp).post('/api/v1/repo').set('Cookie', cookie).send(TEST_GITLAB_REPO);
+ await request(apiApp)
+ .post('/api/v1/repo')
+ .set('Cookie', cookie)
+ .send(toCreateBody(TEST_GITLAB_REPO));
repo = await db.getRepoByUrl(TEST_GITLAB_REPO.url);
}
expect(repo).not.toBeNull();
diff --git a/test/testPush.test.ts b/test/testPush.test.ts
index 8bf85788d..622d4dc19 100644
--- a/test/testPush.test.ts
+++ b/test/testPush.test.ts
@@ -123,7 +123,7 @@ describe('Push API', () => {
await db.deletePush(TEST_PUSH.id);
vi.resetModules();
- await Service.httpServer.close();
+ await Service.httpServer?.close();
const res = await request(app).post('/api/auth/logout').set('Cookie', `${cookie}`);
expect(res.status).toBe(200);
@@ -162,10 +162,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
@@ -190,10 +186,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: false,
},
],
@@ -219,10 +211,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
@@ -251,10 +239,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
@@ -276,10 +260,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
@@ -292,7 +272,7 @@ describe('Push API', () => {
it('should return 401 if not logged in when approving a push', async () => {
const res = await request(app)
.post(`/api/v1/push/${TEST_PUSH.id}/authorise`)
- .send({ reason: 'Testing approval' });
+ .send({ params: { attestation: [] } });
expect(res.status).toBe(401);
expect(res.body.message).toBe('Not logged in');
});
@@ -315,7 +295,11 @@ describe('Push API', () => {
.set('Cookie', `${cookie}`)
.send({});
expect(res.status).toBe(400);
- expect(res.body.message).toBe('Rejection reason is required');
+ expect(res.body.message).toBe('Validation failed');
+ expect(res.body.details).toBeDefined();
+ console.log(res.body.details);
+ expect(res.body.details['body.reason']).toBeDefined();
+ expect(res.body.details['body.reason'].message).toBe("'reason' is required");
});
it('should NOT allow an authorizer to reject a push with empty reason', async () => {
@@ -404,10 +388,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
@@ -429,10 +409,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
diff --git a/test/testRepoApi.test.ts b/test/testRepoApi.test.ts
index 11e4cf6bf..7d0d4c60b 100644
--- a/test/testRepoApi.test.ts
+++ b/test/testRepoApi.test.ts
@@ -44,6 +44,13 @@ const TEST_REPO_NAKED = {
host: '123.456.789:80',
};
+// `host` is only kept on the fixtures above as an expected value for
+// getAllProxiedHosts() assertions; the API rejects it as an unknown property.
+const toCreateBody = (repo: T): Omit => {
+ const { host: _host, ...rest } = repo;
+ return rest;
+};
+
const cleanupRepo = async (url: string) => {
const repo = await db.getRepoByUrl(url);
if (repo) {
@@ -77,7 +84,10 @@ describe('add new repo', () => {
const ensureTestRepoExists = async () => {
let repo = await db.getRepoByUrl(TEST_REPO.url);
if (!repo) {
- await request(app).post('/api/v1/repo').set('Cookie', `${cookie}`).send(TEST_REPO);
+ await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', `${cookie}`)
+ .send(toCreateBody(TEST_REPO));
repo = await db.getRepoByUrl(TEST_REPO.url);
}
if (repo) repoIds[0] = repo._id!;
@@ -124,7 +134,10 @@ describe('add new repo', () => {
// Ensure repo doesn't exist
await cleanupRepo(TEST_REPO.url);
- const res = await request(app).post('/api/v1/repo').set('Cookie', `${cookie}`).send(TEST_REPO);
+ const res = await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', `${cookie}`)
+ .send(toCreateBody(TEST_REPO));
expect(res.status).toBe(200);
const repo = await fetchRepoOrThrow(TEST_REPO.url);
@@ -155,7 +168,10 @@ describe('add new repo', () => {
it('return a 409 error if the repo already exists', async () => {
await ensureTestRepoExists();
- const res = await request(app).post('/api/v1/repo').set('Cookie', `${cookie}`).send(TEST_REPO);
+ const res = await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', `${cookie}`)
+ .send(toCreateBody(TEST_REPO));
expect(res.status).toBe(409);
expect(res.body.message).toBe('Repository ' + TEST_REPO.url + ' already exists!');
});
@@ -349,7 +365,7 @@ describe('add new repo', () => {
const res = await request(app)
.post('/api/v1/repo')
.set('Cookie', cookie)
- .send(TEST_REPO_NON_GITHUB);
+ .send(toCreateBody(TEST_REPO_NON_GITHUB));
expect(res.status).toBe(200);
const repo = await fetchRepoOrThrow(TEST_REPO_NON_GITHUB.url);
@@ -367,7 +383,7 @@ describe('add new repo', () => {
const res2 = await request(app)
.post('/api/v1/repo')
.set('Cookie', cookie)
- .send(TEST_REPO_NAKED);
+ .send(toCreateBody(TEST_REPO_NAKED));
expect(res2.status).toBe(200);
const repo2 = await fetchRepoOrThrow(TEST_REPO_NAKED.url);
@@ -383,14 +399,20 @@ describe('add new repo', () => {
// Ensure repos exist before deleting
let repo1 = await db.getRepoByUrl(TEST_REPO_NON_GITHUB.url);
if (!repo1) {
- await request(app).post('/api/v1/repo').set('Cookie', cookie).send(TEST_REPO_NON_GITHUB);
+ await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', cookie)
+ .send(toCreateBody(TEST_REPO_NON_GITHUB));
repo1 = await db.getRepoByUrl(TEST_REPO_NON_GITHUB.url);
}
repoIds[1] = repo1!._id!;
let repo2 = await db.getRepoByUrl(TEST_REPO_NAKED.url);
if (!repo2) {
- await request(app).post('/api/v1/repo').set('Cookie', cookie).send(TEST_REPO_NAKED);
+ await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', cookie)
+ .send(toCreateBody(TEST_REPO_NAKED));
repo2 = await db.getRepoByUrl(TEST_REPO_NAKED.url);
}
repoIds[2] = repo2!._id!;
@@ -466,7 +488,10 @@ describe('repo routes - edge cases', () => {
setCookie(nonAdminRes, 'nonAdmin');
// Create a test repo
- await request(app).post('/api/v1/repo').set('Cookie', adminCookie).send(TEST_REPO);
+ await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', adminCookie)
+ .send(toCreateBody(TEST_REPO));
const repo = await fetchRepoOrThrow(TEST_REPO.url);
repoId = repo._id!;
@@ -477,7 +502,6 @@ describe('repo routes - edge cases', () => {
url: 'https://github.com/test/unauthorized-repo.git',
name: 'unauthorized-repo',
project: 'test',
- host: 'github.com',
});
expect(res.status).toBe(401);
@@ -489,7 +513,6 @@ describe('repo routes - edge cases', () => {
url: 'https://github.com/test/unauthenticated-repo.git',
name: 'unauthenticated-repo',
project: 'test',
- host: 'github.com',
});
expect(res.status).toBe(401);
@@ -500,11 +523,13 @@ describe('repo routes - edge cases', () => {
const res = await request(app).post('/api/v1/repo').set('Cookie', adminCookie).send({
name: 'no-url-repo',
project: 'test',
- host: 'github.com',
});
expect(res.status).toBe(400);
- expect(res.body.message).toBe('Repository url is required');
+ expect(res.body.message).toBe('Validation failed');
+ expect(res.body.details).toBeDefined();
+ expect(res.body.details['body.url']).toBeDefined();
+ expect(res.body.details['body.url'].message).toBe("'url' is required");
});
it('should return 400 when repo url is invalid', async () => {
@@ -512,7 +537,6 @@ describe('repo routes - edge cases', () => {
url: '',
name: 'invalid-repo',
project: 'test',
- host: 'github.com',
});
expect(res.status).toBe(400);
expect(res.body.message).toBe('Repository url is required');
@@ -678,6 +702,6 @@ describe('repo routes - edge cases', () => {
await cleanupRepo(TEST_REPO.url);
await db.deleteUser('testuser');
await db.deleteUser('nonadmin');
- await Service.httpServer.close();
+ await Service.httpServer?.close();
});
});
diff --git a/tsconfig.json b/tsconfig.json
index 331c876ef..6ba296d3b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -19,7 +19,9 @@
"outDir": "./dist",
"rootDir": "./src",
"noEmit": false,
- "types": ["node"]
+ "types": ["node"],
+
+ "experimentalDecorators": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
diff --git a/tsoa.json b/tsoa.json
new file mode 100644
index 000000000..b58265054
--- /dev/null
+++ b/tsoa.json
@@ -0,0 +1,26 @@
+{
+ "entryFile": "src/app.ts",
+ "noImplicitAdditionalProperties": "throw-on-extras",
+ "controllerPathGlobs": ["src/service/controllers/**/*Controller.ts"],
+ "spec": {
+ "outputDirectory": "dist",
+ "specVersion": 3,
+ "securityDefinitions": {
+ "jwt": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT"
+ }
+ },
+ "info": {
+ "title": "Git Proxy API",
+ "version": "1.0.0"
+ }
+ },
+ "routes": {
+ "routesDir": "src/service",
+ "routesFileName": "generatedRoutes.ts",
+ "authenticationModule": "./src/service/authentication",
+ "middleware": "express"
+ }
+}
diff --git a/website/docs/api/auth.mdx b/website/docs/api/auth.mdx
new file mode 100644
index 000000000..baf238f2c
--- /dev/null
+++ b/website/docs/api/auth.mdx
@@ -0,0 +1,127 @@
+---
+title: Auth
+description: Auth API endpoints for GitProxy
+---
+
+# Auth
+
+*9 endpoints — 5 GET, 4 POST*
+
+## GET `/api/auth`
+
+Returns links to the available authentication resource endpoints.
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/auth/config`
+
+Returns the enabled authentication methods available to the UI.
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/auth/openidconnect`
+
+Initiates the OpenID Connect authentication flow (redirects to the OIDC provider).
+
+#### Responses
+
+- 204 No content
+
+---
+
+## GET `/api/auth/openidconnect/callback`
+
+OpenID Connect callback — exchanges the authorization code for a session.
+
+#### Responses
+
+- 204 No content
+
+---
+
+## GET `/api/auth/profile`
+
+Returns the profile of the currently authenticated user.
+
+#### Responses
+
+- 200 Ok
+- 401 Unauthorized
+- 404 Not Found
+
+---
+
+## POST `/api/auth/create-user`
+
+Creates a new user. Requires admin privileges.
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `username` | `string` | **Yes** | |
+| `password` | `string` | **Yes** | |
+| `email` | `string` | **Yes** | |
+| `gitAccount` | `string` | **Yes** | |
+| `admin` | `boolean` | No | |
+
+#### Responses
+
+- 200 Ok
+- 403 Forbidden
+- 500 Internal Server Error
+
+---
+
+## POST `/api/auth/gitAccount`
+
+Updates the Git account (username) of a user.
+Admins may update any user; non-admins may only update their own account.
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `username` | `string` | No | |
+| `id` | `string` | No | |
+| `gitAccount` | `string` | **Yes** | |
+
+#### Responses
+
+- 204 No content
+- 400 Bad Request
+- 401 Unauthorized
+- 403 Forbidden
+- 404 Not Found
+- 500 Internal Server Error
+
+---
+
+## POST `/api/auth/login`
+
+Authenticates the user with a username/password strategy.
+The appropriate passport strategy is selected dynamically based on configuration.
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## POST `/api/auth/logout`
+
+Logs out the current user and clears the session cookie.
+
+#### Responses
+
+- 200 Ok
+
+---
diff --git a/website/docs/api/config.mdx b/website/docs/api/config.mdx
new file mode 100644
index 000000000..b7df40d10
--- /dev/null
+++ b/website/docs/api/config.mdx
@@ -0,0 +1,40 @@
+---
+title: Config
+description: Config API endpoints for GitProxy
+---
+
+# Config
+
+*4 endpoints — 4 GET*
+
+## GET `/api/v1/config/attestation`
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/config/contactEmail`
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/config/uiRouteAuth`
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/config/urlShortener`
+
+#### Responses
+
+- 200 Ok
+
+---
diff --git a/website/docs/api/health.mdx b/website/docs/api/health.mdx
new file mode 100644
index 000000000..98c797433
--- /dev/null
+++ b/website/docs/api/health.mdx
@@ -0,0 +1,16 @@
+---
+title: Health
+description: Health API endpoints for GitProxy
+---
+
+# Health
+
+*1 endpoint — 1 GET*
+
+## GET `/api/v1/healthcheck`
+
+#### Responses
+
+- 200 Ok
+
+---
diff --git a/website/docs/api/home.mdx b/website/docs/api/home.mdx
new file mode 100644
index 000000000..098b3a49d
--- /dev/null
+++ b/website/docs/api/home.mdx
@@ -0,0 +1,16 @@
+---
+title: Home
+description: Home API endpoints for GitProxy
+---
+
+# Home
+
+*1 endpoint — 1 GET*
+
+## GET `/api`
+
+#### Responses
+
+- 200 Ok
+
+---
diff --git a/website/docs/api/index.mdx b/website/docs/api/index.mdx
new file mode 100644
index 000000000..ec03d2784
--- /dev/null
+++ b/website/docs/api/index.mdx
@@ -0,0 +1,32 @@
+---
+title: API Reference
+description: REST API reference documentation for GitProxy
+---
+
+# API Reference
+
+Deploy custom push protections and policies on top of Git.
+
+:::info[API Version]
+**2.0.0-rc.6** — OpenAPI 3.0
+:::
+
+## Authentication
+
+Most endpoints require a **BEARER** token (JWT) passed via the `Authorization` header:
+
+```
+Authorization: Bearer
+```
+
+## Endpoints
+
+30 endpoints across 7 groups:
+
+- [**Auth**](/docs/api/auth) — 5 GET, 4 POST
+- [**Config**](/docs/api/config) — 4 GET
+- [**Health**](/docs/api/health) — 1 GET
+- [**Home**](/docs/api/home) — 1 GET
+- [**Push**](/docs/api/push) — 2 GET, 3 POST
+- [**Repositories**](/docs/api/repositories) — 2 GET, 1 POST, 2 PATCH, 3 DELETE
+- [**Users**](/docs/api/users) — 2 GET
diff --git a/website/docs/api/push.mdx b/website/docs/api/push.mdx
new file mode 100644
index 000000000..e5439d111
--- /dev/null
+++ b/website/docs/api/push.mdx
@@ -0,0 +1,126 @@
+---
+title: Push
+description: Push API endpoints for GitProxy
+---
+
+# Push
+
+*5 endpoints — 2 GET, 3 POST*
+
+## GET `/api/v1/push`AUTH
+
+Returns push requests, optionally filtered by query parameters.
+Supported filters: any field from PushQuery (error, blocked, allowPush, authorised, canceled, rejected, type).
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/push/{id}`AUTH
+
+Returns a single push request by ID.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 404 Not Found
+
+---
+
+## POST `/api/v1/push/{id}/authorise`AUTH
+
+Authorises (approves) a pending push request.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `params` | `object` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+- 403 Forbidden
+- 404 Not Found
+
+---
+
+## POST `/api/v1/push/{id}/cancel`AUTH
+
+Cancels a pending push request.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 401 Unauthorized
+- 403 Forbidden
+
+---
+
+## POST `/api/v1/push/{id}/reject`AUTH
+
+Rejects a pending push request.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `reason` | `string` | **Yes** | The reason for rejecting the push request. |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+- 403 Forbidden
+- 404 Not Found
+
+---
diff --git a/website/docs/api/repositories.mdx b/website/docs/api/repositories.mdx
new file mode 100644
index 000000000..64a90be96
--- /dev/null
+++ b/website/docs/api/repositories.mdx
@@ -0,0 +1,192 @@
+---
+title: Repositories
+description: Repositories API endpoints for GitProxy
+---
+
+# Repositories
+
+*8 endpoints — 2 GET, 1 POST, 2 PATCH, 3 DELETE*
+
+## GET `/api/v1/repo`AUTH
+
+Returns repositories, optionally filtered by query parameters.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/repo/{id}`AUTH
+
+Returns a single repository by ID.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 404 Not Found
+
+---
+
+## POST `/api/v1/repo`AUTH
+
+Creates a new repository. May restart the proxy if a new origin is added.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `url` | `string` | **Yes** | |
+| `name` | `string` | **Yes** | |
+| `project` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+- 409 Conflict
+- 500 Internal Server Error
+
+---
+
+## PATCH `/api/v1/repo/{id}/user/authorise`AUTH
+
+Grants a user authorise permission on a repository.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `username` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+
+---
+
+## PATCH `/api/v1/repo/{id}/user/push`AUTH
+
+Grants a user push permission on a repository.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `username` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+
+---
+
+## DELETE `/api/v1/repo/{id}/delete`AUTH
+
+Deletes a repository. May restart the proxy if a proxied host is removed.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 401 Unauthorized
+
+---
+
+## DELETE `/api/v1/repo/{id}/user/authorise/{username}`AUTH
+
+Revokes a user's authorise permission on a repository.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+| `username` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+
+---
+
+## DELETE `/api/v1/repo/{id}/user/push/{username}`AUTH
+
+Revokes a user's push permission on a repository.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+| `username` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+
+---
diff --git a/website/docs/api/users.mdx b/website/docs/api/users.mdx
new file mode 100644
index 000000000..e1c25ceaf
--- /dev/null
+++ b/website/docs/api/users.mdx
@@ -0,0 +1,43 @@
+---
+title: Users
+description: Users API endpoints for GitProxy
+---
+
+# Users
+
+*2 endpoints — 2 GET*
+
+## GET `/api/v1/user`AUTH
+
+Returns all registered users (public fields only).
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/user/{id}`AUTH
+
+Returns a single user by username.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 404 Not Found
+
+---
diff --git a/website/docs/architecture/architecture.md b/website/docs/architecture/architecture.md
index 97da3ce38..7e70974f8 100644
--- a/website/docs/architecture/architecture.md
+++ b/website/docs/architecture/architecture.md
@@ -452,7 +452,7 @@ Allows defining ways to authenticate to the API. This is useful for securing cus
If `apiAuthentication` is left empty, API endpoints will be publicly accesible.
-Currently, only JWT auth is supported. This is implemented via the [`jwtAuthHandler` middleware](https://github.com/finos/git-proxy/blob/main/src/service/passport/jwtAuthHandler.ts). Aside of validating incoming access tokens, it can also assign roles based on the token payload.
+Currently, only JWT auth is supported. This is implemented via the [`@Security('jwt')` decorator](https://github.com/finos/git-proxy/blob/main/src/service/authentication.ts). Aside of validating incoming access tokens, it can also assign roles based on the token payload.
##### Setting up JWT Authentication
diff --git a/website/sidebars.js b/website/sidebars.js
index aa9bb948a..ffbf16cbc 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -52,6 +52,25 @@ module.exports = {
collapsed: false,
items: ['configuration/overview', 'configuration/reference', 'configuration/pre-receive'],
},
+ {
+ type: 'category',
+ label: 'API Reference',
+ link: {
+ type: 'doc',
+ id: 'api/index',
+ },
+ collapsible: true,
+ collapsed: false,
+ items: [
+ 'api/auth',
+ 'api/config',
+ 'api/health',
+ 'api/home',
+ 'api/push',
+ 'api/repositories',
+ 'api/users',
+ ],
+ },
{
type: 'category',
label: 'Architecture',
@@ -66,6 +85,7 @@ module.exports = {
collapsed: false,
items: ['architecture/architecture', 'architecture/processors'],
},
+ 'deployment',
{
type: 'category',
label: 'Development',