Add OAuth logic for gitea
This commit is contained in:
parent
1a59db63cd
commit
0eab62158c
@ -19,7 +19,9 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.0",
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/express-session": "^1.18.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/pg": "^8.15.5",
|
||||
@ -29,7 +31,11 @@
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"arctic": "^3.7.0",
|
||||
"axios": "^1.7.9",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"express": "^5.1.0",
|
||||
"express-session": "^1.18.0",
|
||||
"openapi-backend": "^5.15.0",
|
||||
"pg": "^8.16.3",
|
||||
"postgres-migrations": "^5.3.0"
|
||||
|
||||
244
server/pnpm-lock.yaml
generated
244
server/pnpm-lock.yaml
generated
@ -8,9 +8,21 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
arctic:
|
||||
specifier: ^3.7.0
|
||||
version: 3.7.0
|
||||
axios:
|
||||
specifier: ^1.7.9
|
||||
version: 1.11.0
|
||||
cookie-parser:
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.7
|
||||
express:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
express-session:
|
||||
specifier: ^1.18.0
|
||||
version: 1.18.2
|
||||
openapi-backend:
|
||||
specifier: ^5.15.0
|
||||
version: 5.15.0
|
||||
@ -27,9 +39,15 @@ importers:
|
||||
'@tsconfig/strictest':
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.5
|
||||
'@types/cookie-parser':
|
||||
specifier: ^1.4.9
|
||||
version: 1.4.9(@types/express@5.0.3)
|
||||
'@types/express':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
'@types/express-session':
|
||||
specifier: ^1.18.0
|
||||
version: 1.18.2
|
||||
'@types/jest':
|
||||
specifier: ^30.0.0
|
||||
version: 30.0.0
|
||||
@ -558,6 +576,24 @@ packages:
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
'@oslojs/asn1@1.0.0':
|
||||
resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==}
|
||||
|
||||
'@oslojs/binary@1.0.0':
|
||||
resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==}
|
||||
|
||||
'@oslojs/crypto@1.0.1':
|
||||
resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==}
|
||||
|
||||
'@oslojs/encoding@0.4.1':
|
||||
resolution: {integrity: sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==}
|
||||
|
||||
'@oslojs/encoding@1.1.0':
|
||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||
|
||||
'@oslojs/jwt@0.2.0':
|
||||
resolution: {integrity: sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
@ -599,9 +635,17 @@ packages:
|
||||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/cookie-parser@1.4.9':
|
||||
resolution: {integrity: sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==}
|
||||
peerDependencies:
|
||||
'@types/express': '*'
|
||||
|
||||
'@types/express-serve-static-core@5.0.7':
|
||||
resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==}
|
||||
|
||||
'@types/express-session@1.18.2':
|
||||
resolution: {integrity: sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==}
|
||||
|
||||
'@types/express@5.0.3':
|
||||
resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==}
|
||||
|
||||
@ -794,12 +838,21 @@ packages:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
arctic@3.7.0:
|
||||
resolution: {integrity: sha512-ZMQ+f6VazDgUJOd+qNV+H7GohNSYal1mVjm5kEaZfE2Ifb7Ss70w+Q7xpJC87qZDkMZIXYf0pTIYZA0OPasSbw==}
|
||||
|
||||
argparse@1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
axios@1.11.0:
|
||||
resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
|
||||
|
||||
babel-jest@30.0.5:
|
||||
resolution: {integrity: sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
@ -920,6 +973,10 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
@ -934,6 +991,16 @@ packages:
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
cookie-parser@1.4.7:
|
||||
resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
|
||||
cookie-signature@1.0.7:
|
||||
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
|
||||
|
||||
cookie-signature@1.2.2:
|
||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||
engines: {node: '>=6.6.0'}
|
||||
@ -950,6 +1017,14 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
debug@4.4.1:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@ -971,6 +1046,10 @@ packages:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -1024,6 +1103,10 @@ packages:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild@0.25.9:
|
||||
resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1061,6 +1144,10 @@ packages:
|
||||
resolution: {integrity: sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
express-session@1.18.2:
|
||||
resolution: {integrity: sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
express@5.1.0:
|
||||
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
|
||||
engines: {node: '>= 18'}
|
||||
@ -1089,10 +1176,23 @@ packages:
|
||||
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
follow-redirects@1.15.11:
|
||||
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.4:
|
||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -1167,6 +1267,10 @@ packages:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1464,10 +1568,18 @@ packages:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-db@1.54.0:
|
||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@3.0.1:
|
||||
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -1493,6 +1605,9 @@ packages:
|
||||
mock-json-schema@1.1.1:
|
||||
resolution: {integrity: sha512-YV23vlsLP1EEOy0EviUvZTluXjLR+rhMzeayP2rcDiezj3RW01MhOSQkbQskdtg0K2fnGas5LKbSXgNjAOSX4A==}
|
||||
|
||||
ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@ -1533,6 +1648,10 @@ packages:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
on-headers@1.1.0:
|
||||
resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
@ -1679,6 +1798,9 @@ packages:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
pure-rand@7.0.1:
|
||||
resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==}
|
||||
|
||||
@ -1686,6 +1808,10 @@ packages:
|
||||
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
random-bytes@1.0.0:
|
||||
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -1931,6 +2057,10 @@ packages:
|
||||
engines: {node: '>=0.8.0'}
|
||||
hasBin: true
|
||||
|
||||
uid-safe@2.1.5:
|
||||
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
undici-types@7.10.0:
|
||||
resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
|
||||
|
||||
@ -2556,6 +2686,25 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.0
|
||||
optional: true
|
||||
|
||||
'@oslojs/asn1@1.0.0':
|
||||
dependencies:
|
||||
'@oslojs/binary': 1.0.0
|
||||
|
||||
'@oslojs/binary@1.0.0': {}
|
||||
|
||||
'@oslojs/crypto@1.0.1':
|
||||
dependencies:
|
||||
'@oslojs/asn1': 1.0.0
|
||||
'@oslojs/binary': 1.0.0
|
||||
|
||||
'@oslojs/encoding@0.4.1': {}
|
||||
|
||||
'@oslojs/encoding@1.1.0': {}
|
||||
|
||||
'@oslojs/jwt@0.2.0':
|
||||
dependencies:
|
||||
'@oslojs/encoding': 0.4.1
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
@ -2608,6 +2757,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 24.3.0
|
||||
|
||||
'@types/cookie-parser@1.4.9(@types/express@5.0.3)':
|
||||
dependencies:
|
||||
'@types/express': 5.0.3
|
||||
|
||||
'@types/express-serve-static-core@5.0.7':
|
||||
dependencies:
|
||||
'@types/node': 24.3.0
|
||||
@ -2615,6 +2768,10 @@ snapshots:
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 0.17.5
|
||||
|
||||
'@types/express-session@1.18.2':
|
||||
dependencies:
|
||||
'@types/express': 5.0.3
|
||||
|
||||
'@types/express@5.0.3':
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.6
|
||||
@ -2773,12 +2930,28 @@ snapshots:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
|
||||
arctic@3.7.0:
|
||||
dependencies:
|
||||
'@oslojs/crypto': 1.0.1
|
||||
'@oslojs/encoding': 1.1.0
|
||||
'@oslojs/jwt': 0.2.0
|
||||
|
||||
argparse@1.0.10:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
axios@1.11.0:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
form-data: 4.0.4
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
babel-jest@30.0.5(@babel/core@7.28.3):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.3
|
||||
@ -2928,6 +3101,10 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
content-disposition@1.0.0:
|
||||
@ -2938,6 +3115,15 @@ snapshots:
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie-parser@1.4.7:
|
||||
dependencies:
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.0.6
|
||||
|
||||
cookie-signature@1.0.6: {}
|
||||
|
||||
cookie-signature@1.0.7: {}
|
||||
|
||||
cookie-signature@1.2.2: {}
|
||||
|
||||
cookie@0.7.2: {}
|
||||
@ -2950,6 +3136,10 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@ -2958,6 +3148,8 @@ snapshots:
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
dereference-json-schema@0.2.1: {}
|
||||
@ -2996,6 +3188,13 @@ snapshots:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.2
|
||||
|
||||
esbuild@0.25.9:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.9
|
||||
@ -3058,6 +3257,19 @@ snapshots:
|
||||
jest-mock: 30.0.5
|
||||
jest-util: 30.0.5
|
||||
|
||||
express-session@1.18.2:
|
||||
dependencies:
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.0.7
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
on-headers: 1.1.0
|
||||
parseurl: 1.3.3
|
||||
safe-buffer: 5.2.1
|
||||
uid-safe: 2.1.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
express@5.1.0:
|
||||
dependencies:
|
||||
accepts: 2.0.0
|
||||
@ -3120,11 +3332,21 @@ snapshots:
|
||||
locate-path: 5.0.0
|
||||
path-exists: 4.0.0
|
||||
|
||||
follow-redirects@1.15.11: {}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data@4.0.4:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
fresh@2.0.0: {}
|
||||
@ -3201,6 +3423,10 @@ snapshots:
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
@ -3661,8 +3887,14 @@ snapshots:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-db@1.54.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
mime-types@3.0.1:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
@ -3685,6 +3917,8 @@ snapshots:
|
||||
dependencies:
|
||||
lodash: 4.17.21
|
||||
|
||||
ms@2.0.0: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
napi-postinstall@0.3.3: {}
|
||||
@ -3711,6 +3945,8 @@ snapshots:
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
|
||||
on-headers@1.1.0: {}
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
@ -3854,12 +4090,16 @@ snapshots:
|
||||
forwarded: 0.2.0
|
||||
ipaddr.js: 1.9.1
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
pure-rand@7.0.1: {}
|
||||
|
||||
qs@6.14.0:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
random-bytes@1.0.0: {}
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
raw-body@3.0.0:
|
||||
@ -4093,6 +4333,10 @@ snapshots:
|
||||
uglify-js@3.19.3:
|
||||
optional: true
|
||||
|
||||
uid-safe@2.1.5:
|
||||
dependencies:
|
||||
random-bytes: 1.0.0
|
||||
|
||||
undici-types@7.10.0: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
@ -8,6 +8,14 @@ interface ProcessEnv {
|
||||
DB_HOST?: string;
|
||||
DB_NAME: string;
|
||||
DB_PORT?: string;
|
||||
|
||||
GITEA_URL?: string;
|
||||
GITEA_CLIENT_ID?: string;
|
||||
GITEA_CLIENT_SECRET?: string;
|
||||
GITEA_REDIRECT_URI?: string;
|
||||
|
||||
FORGE_TYPES?: string; // comma-separated list of enabled forges
|
||||
SESSION_SECRET: string;
|
||||
}
|
||||
|
||||
const env = process.env as unknown as ProcessEnv;
|
||||
@ -28,6 +36,20 @@ export const config = {
|
||||
DB_USER: requiredEnv("DB_USER"),
|
||||
DB_PASSWORD: requiredEnv("DB_PASSWORD"),
|
||||
DB_PORT: env.DB_PORT ? Number(env.DB_PORT) : 5432,
|
||||
|
||||
GITEA_URL: env.GITEA_URL ?? "http://localhost:3001",
|
||||
GITEA_CLIENT_ID: env.GITEA_CLIENT_ID,
|
||||
GITEA_CLIENT_SECRET: env.GITEA_CLIENT_SECRET,
|
||||
GITEA_REDIRECT_URI:
|
||||
env.GITEA_REDIRECT_URI ?? "http://localhost:3000/auth/callback",
|
||||
|
||||
FORGE_TYPES: (env.FORGE_TYPES ?? "gitea")
|
||||
.split(",")
|
||||
.map((f) => f.trim().toLowerCase()),
|
||||
|
||||
SESSION_SECRET: requiredEnv("SESSION_SECRET"),
|
||||
|
||||
IS_PRODUCTION: env.NODE_ENV === "production",
|
||||
};
|
||||
|
||||
export const isProduction = config.NODE_ENV === "production";
|
||||
|
||||
90
server/src/routes/auth.ts
Normal file
90
server/src/routes/auth.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { createForge, listAvailableForges } from "../services/forges";
|
||||
import { config } from "../config/env";
|
||||
|
||||
const authRouter = Router();
|
||||
|
||||
authRouter.get("/providers", (_req, res) => {
|
||||
res.json({ providers: listAvailableForges() });
|
||||
});
|
||||
|
||||
authRouter.get("/login/:forgeType", (req: Request, res: Response) => {
|
||||
const forgeType = req.params["forgeType"];
|
||||
if (!forgeType) return res.status(400).json({ error: "Missing forgeType" });
|
||||
|
||||
try {
|
||||
const forge = createForge(forgeType);
|
||||
const authData = forge.getAuthorizationUrl();
|
||||
|
||||
req.session.codeVerifier = authData.codeVerifier;
|
||||
req.session.forgeType = forgeType;
|
||||
|
||||
res.cookie("state", authData.state, {
|
||||
secure: config.IS_PRODUCTION,
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
maxAge: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
|
||||
res.json({ authUrl: authData.url });
|
||||
} catch (err) {
|
||||
console.error("Failed to generate auth URL:", err);
|
||||
res.status(400).json({ error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
authRouter.get("/callback", async (req: Request, res: Response) => {
|
||||
const forgeType = req.session.forgeType;
|
||||
const codeVerifier = req.session.codeVerifier;
|
||||
const state = req.query["state"] as string;
|
||||
const code = req.query["code"] as string;
|
||||
|
||||
if (!forgeType || !state || !code || !codeVerifier) {
|
||||
return res.status(400).json({ error: "Missing OAuth callback parameters" });
|
||||
}
|
||||
|
||||
try {
|
||||
const forge = createForge(forgeType);
|
||||
|
||||
const { accessToken } = await forge.exchangeCodeForToken(
|
||||
code,
|
||||
codeVerifier,
|
||||
);
|
||||
|
||||
const userInfo = await forge.getUserInfo(accessToken);
|
||||
|
||||
// -----------------------------
|
||||
// TODO: Insert user in DB
|
||||
// -----------------------------
|
||||
const simulatedUserId = 1; // replace with actual DB user_id
|
||||
|
||||
req.session.userId = simulatedUserId;
|
||||
req.session.forgeType = forgeType;
|
||||
|
||||
// Clear OAuth session data
|
||||
delete req.session.codeVerifier;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: simulatedUserId, // internal molci ID
|
||||
forgeUserId: userInfo.id, // forge user ID
|
||||
login: userInfo.login,
|
||||
avatar_url: userInfo.avatar_url,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("OAuth callback error:", err);
|
||||
res.status(500).json({ error: "OAuth callback failed" });
|
||||
}
|
||||
});
|
||||
|
||||
authRouter.post("/logout", (req: Request, res: Response) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) return res.status(500).json({ error: "Failed to logout" });
|
||||
res.clearCookie("connect.sid");
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
export default authRouter;
|
||||
@ -1,8 +1,35 @@
|
||||
import express from "express";
|
||||
import { connectDB, pool } from "./config/db";
|
||||
import express, {
|
||||
type Request as ExpressRequest,
|
||||
type Response as ExpressResponse,
|
||||
} from "express";
|
||||
import cookieParser from "cookie-parser";
|
||||
import session from "express-session";
|
||||
import { config, isProduction } from "./config/env";
|
||||
import { connectDB } from "./config/db";
|
||||
import { OpenAPIBackend, type Request } from "openapi-backend";
|
||||
import type { Repository } from "./types/openapi";
|
||||
import { createForge } from "./services/forges";
|
||||
import authRouter from "./routes/auth";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
|
||||
// express session for Session cookie on the browser and Session object on the server
|
||||
app.use(
|
||||
session({
|
||||
secret: config.SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: config.IS_PRODUCTION,
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000, // 1 day
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
app.use("/auth", authRouter);
|
||||
|
||||
|
||||
const api = new OpenAPIBackend({
|
||||
definition: "./openapi.yaml",
|
||||
@ -10,37 +37,35 @@ const api = new OpenAPIBackend({
|
||||
});
|
||||
|
||||
api.register({
|
||||
listAvailableRepos: async (
|
||||
_c,
|
||||
_req: express.Request,
|
||||
res: express.Response,
|
||||
) => {
|
||||
const repos: Repository[] = [];
|
||||
return res.json(repos);
|
||||
listAvailableRepos: async (_c, req: ExpressRequest, res: ExpressResponse) => {
|
||||
try {
|
||||
const userId = req.session?.userId;
|
||||
const forgeType = req.session?.forgeType;
|
||||
|
||||
if (!userId || !forgeType) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
// TODO: Fetch user from DB here
|
||||
const simulatedAccessToken = "FAKE_TOKEN"; // remove once DB is used
|
||||
|
||||
const forge = createForge(forgeType);
|
||||
const repos = await forge.listRepositories(simulatedAccessToken);
|
||||
|
||||
return res.json(repos);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch repos:", err);
|
||||
return res.status(500).json({ error: "Failed to list repositories" });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.get("/ping", async (_req, res) => {
|
||||
try {
|
||||
const result = await pool.query("SELECT NOW()");
|
||||
res.json({ success: true, time: result.rows[0].now });
|
||||
} catch (error) {
|
||||
console.error("Database error:", error);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, error: "Database connection failed" });
|
||||
}
|
||||
});
|
||||
|
||||
api.init();
|
||||
app.use((req, res) => api.handleRequest(req as Request, req, res));
|
||||
|
||||
connectDB();
|
||||
|
||||
|
||||
const PORT = config.APP_PORT;
|
||||
app.listen(PORT, () => {
|
||||
console.log(
|
||||
|
||||
65
server/src/services/forges/gitea.ts
Normal file
65
server/src/services/forges/gitea.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import * as arctic from "arctic";
|
||||
import { config } from "../../config/env";
|
||||
import type { Forge } from "../../types/forge";
|
||||
import type { Repository } from "../../types/openapi";
|
||||
import type { User } from "../../types/user";
|
||||
|
||||
export class GiteaForge implements Forge {
|
||||
private gitea: arctic.Gitea;
|
||||
|
||||
constructor() {
|
||||
if (!config.GITEA_CLIENT_ID || !config.GITEA_CLIENT_SECRET) {
|
||||
throw new Error("Gitea OAuth2 credentials not configured");
|
||||
}
|
||||
|
||||
this.gitea = new arctic.Gitea(
|
||||
config.GITEA_URL,
|
||||
config.GITEA_CLIENT_ID,
|
||||
config.GITEA_CLIENT_SECRET,
|
||||
config.GITEA_REDIRECT_URI,
|
||||
);
|
||||
}
|
||||
|
||||
getAuthorizationUrl() {
|
||||
const state = arctic.generateState();
|
||||
const codeVerifier = arctic.generateCodeVerifier();
|
||||
const scopes = ["read:user"];
|
||||
const url = this.gitea.createAuthorizationURL(state, codeVerifier, scopes);
|
||||
|
||||
return { url: url.toString(), state, codeVerifier };
|
||||
}
|
||||
|
||||
async exchangeCodeForToken(code: string, codeVerifier: string) {
|
||||
const tokens = await this.gitea.validateAuthorizationCode(
|
||||
code,
|
||||
codeVerifier,
|
||||
);
|
||||
return { accessToken: tokens.accessToken() };
|
||||
}
|
||||
|
||||
async getUserInfo(accessToken: string): Promise<User> {
|
||||
const res = await fetch(`${config.GITEA_URL}/api/v1/user`, {
|
||||
headers: { Authorization: `token ${accessToken}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to fetch user: ${res.status}`);
|
||||
const user = await res.json();
|
||||
return { id: user.id, login: user.login, avatar_url: user.avatar_url };
|
||||
}
|
||||
|
||||
async listRepositories(accessToken: string): Promise<Repository[]> {
|
||||
const res = await fetch(`${config.GITEA_URL}/api/v1/user/repos`, {
|
||||
headers: { Authorization: `token ${accessToken}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to fetch repos: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async validateToken(accessToken: string) {
|
||||
try {
|
||||
await this.getUserInfo(accessToken);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
server/src/services/forges/index.ts
Normal file
31
server/src/services/forges/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { GiteaForge } from "./gitea";
|
||||
import type { Forge } from "../../types/forge";
|
||||
import { config } from "../../config/env";
|
||||
|
||||
/**
|
||||
* Returns a Forge implementation for the given forgeType.
|
||||
* Throws an error if the forge is not enabled or not implemented.
|
||||
*/
|
||||
export function createForge(forgeType: string): Forge {
|
||||
if (!config.FORGE_TYPES.includes(forgeType)) {
|
||||
throw new Error(`Unsupported forge type: ${forgeType}`);
|
||||
}
|
||||
|
||||
switch (forgeType) {
|
||||
case "gitea":
|
||||
return new GiteaForge();
|
||||
case "github":
|
||||
throw new Error("GitHubForge not implemented yet");
|
||||
case "gitlab":
|
||||
throw new Error("GitLabForge not implemented yet");
|
||||
default:
|
||||
throw new Error(`Unsupported forge type: ${forgeType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of available forge types from config.
|
||||
*/
|
||||
export function listAvailableForges(): string[] {
|
||||
return [...config.FORGE_TYPES];
|
||||
}
|
||||
88
server/src/services/oauth.ts
Normal file
88
server/src/services/oauth.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import * as arctic from "arctic";
|
||||
import { config } from "../config/env";
|
||||
import type { User } from "../types/user";
|
||||
|
||||
|
||||
export class OAuthService {
|
||||
private gitea: arctic.Gitea;
|
||||
|
||||
constructor() {
|
||||
if (!config.GITEA_CLIENT_ID || !config.GITEA_CLIENT_SECRET) {
|
||||
throw new Error("Gitea OAuth2 credentials not configured");
|
||||
}
|
||||
|
||||
this.gitea = new arctic.Gitea(
|
||||
config.GITEA_URL,
|
||||
config.GITEA_CLIENT_ID,
|
||||
config.GITEA_CLIENT_SECRET,
|
||||
config.GITEA_REDIRECT_URI,
|
||||
);
|
||||
}
|
||||
|
||||
getAuthorizationUrl(): { url: string; state: string; codeVerifier: string } {
|
||||
const state = arctic.generateState();
|
||||
const codeVerifier = arctic.generateCodeVerifier();
|
||||
const scopes = ["read:user"];
|
||||
const url = this.gitea.createAuthorizationURL(state, codeVerifier, scopes);
|
||||
|
||||
return {
|
||||
url: url.toString(),
|
||||
state,
|
||||
codeVerifier,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange authorization code for access token
|
||||
*/
|
||||
// https://arcticjs.dev/guides/oauth2-pkce
|
||||
// https://arcticjs.dev/providers/gitea
|
||||
async exchangeCodeForToken(
|
||||
code: string,
|
||||
codeVerifier: string,
|
||||
): Promise<{ accessToken: string }> {
|
||||
try {
|
||||
const tokens = await this.gitea.validateAuthorizationCode(
|
||||
code,
|
||||
codeVerifier,
|
||||
);
|
||||
return { accessToken: tokens.accessToken() };
|
||||
} catch (error) {
|
||||
console.error("Failed to exchange code for token:", error);
|
||||
throw new Error("Failed to exchange authorisation code for token");
|
||||
}
|
||||
}
|
||||
|
||||
async getUserInfo(accessToken: string): Promise<User> {
|
||||
try {
|
||||
const response = await fetch(`${config.GITEA_URL}/api/v1/user`, {
|
||||
headers: {
|
||||
Authorization: `token ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch user info: ${response.status}`);
|
||||
}
|
||||
|
||||
const user = await response.json();
|
||||
return {
|
||||
id: user.id,
|
||||
login: user.login,
|
||||
avatar_url: user.avatar_url,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to get user info:", error);
|
||||
throw new Error("Failed to get user information");
|
||||
}
|
||||
}
|
||||
|
||||
async validateToken(accessToken: string): Promise<boolean> {
|
||||
try {
|
||||
await this.getUserInfo(accessToken);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
server/src/types/express-session.d.ts
vendored
Normal file
9
server/src/types/express-session.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
import "express-session";
|
||||
|
||||
declare module "express-session" {
|
||||
interface SessionData {
|
||||
userId?: number; // internal DB ID
|
||||
forgeType?: string; // e.g., "gitea" or "github"
|
||||
codeVerifier?: string; // PKCE code verifier for OAuth
|
||||
}
|
||||
}
|
||||
17
server/src/types/forge.ts
Normal file
17
server/src/types/forge.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { User } from "../types/user";
|
||||
import type { Repository } from "../types/openapi";
|
||||
|
||||
export interface Forge {
|
||||
getAuthorizationUrl(): { url: string; state: string; codeVerifier: string };
|
||||
|
||||
exchangeCodeForToken(
|
||||
code: string,
|
||||
codeVerifier: string,
|
||||
): Promise<{ accessToken: string }>;
|
||||
|
||||
getUserInfo(accessToken: string): Promise<User>;
|
||||
|
||||
listRepositories(accessToken: string): Promise<Repository[]>;
|
||||
|
||||
validateToken(accessToken: string): Promise<boolean>;
|
||||
}
|
||||
290
server/src/types/openapi.d.ts
vendored
290
server/src/types/openapi.d.ts
vendored
@ -1,139 +1,175 @@
|
||||
import type {
|
||||
Context,
|
||||
UnknownParams,
|
||||
} from 'openapi-backend';
|
||||
import type { Context, UnknownParams } from "openapi-backend";
|
||||
|
||||
declare namespace Components {
|
||||
namespace Schemas {
|
||||
export interface Error {
|
||||
/**
|
||||
* Error message
|
||||
* example:
|
||||
* Repository not found
|
||||
*/
|
||||
error: string;
|
||||
/**
|
||||
* Error code for programmatic handling
|
||||
* example:
|
||||
* REPO_NOT_FOUND
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* Additional error details
|
||||
*/
|
||||
details?: {
|
||||
[name: string]: any;
|
||||
};
|
||||
}
|
||||
export interface Repository {
|
||||
/**
|
||||
* example:
|
||||
* 123
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* example:
|
||||
* molci
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* example:
|
||||
* gitea
|
||||
*/
|
||||
forge?: "gitea";
|
||||
}
|
||||
export interface RepositoryConfig {
|
||||
repo?: Repository;
|
||||
configured_at?: string; // date-time
|
||||
webhook_id?: string;
|
||||
}
|
||||
export interface RepositoryConfigInput {
|
||||
settings: {
|
||||
[name: string]: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
namespace Schemas {
|
||||
export interface Error {
|
||||
/**
|
||||
* Error message
|
||||
* example:
|
||||
* Repository not found
|
||||
*/
|
||||
error: string;
|
||||
/**
|
||||
* Error code for programmatic handling
|
||||
* example:
|
||||
* REPO_NOT_FOUND
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* Additional error details
|
||||
*/
|
||||
details?: {
|
||||
[name: string]: any;
|
||||
};
|
||||
}
|
||||
export interface Repository {
|
||||
/**
|
||||
* example:
|
||||
* 123
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* example:
|
||||
* molci
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* example:
|
||||
* gitea
|
||||
*/
|
||||
forge?: "gitea";
|
||||
}
|
||||
export interface RepositoryConfig {
|
||||
repo?: Repository;
|
||||
configured_at?: string; // date-time
|
||||
webhook_id?: string;
|
||||
}
|
||||
export interface RepositoryConfigInput {
|
||||
settings: {
|
||||
[name: string]: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
declare namespace Paths {
|
||||
namespace ConfigureRepo {
|
||||
namespace Parameters {
|
||||
export type Id = string;
|
||||
}
|
||||
export interface PathParameters {
|
||||
id: Parameters.Id;
|
||||
}
|
||||
export type RequestBody = Components.Schemas.RepositoryConfigInput;
|
||||
namespace Responses {
|
||||
export type $200 = Components.Schemas.RepositoryConfig;
|
||||
export type $400 = Components.Schemas.Error;
|
||||
export type $401 = Components.Schemas.Error;
|
||||
export type $404 = Components.Schemas.Error;
|
||||
export type $500 = Components.Schemas.Error;
|
||||
}
|
||||
}
|
||||
namespace ListAvailableRepos {
|
||||
namespace Responses {
|
||||
export type $200 = Components.Schemas.Repository[];
|
||||
export type $401 = Components.Schemas.Error;
|
||||
export type $403 = Components.Schemas.Error;
|
||||
export type $500 = Components.Schemas.Error;
|
||||
}
|
||||
}
|
||||
namespace ListConfiguredRepos {
|
||||
namespace Responses {
|
||||
export type $200 = Components.Schemas.RepositoryConfig[];
|
||||
export type $401 = Components.Schemas.Error;
|
||||
export type $500 = Components.Schemas.Error;
|
||||
}
|
||||
}
|
||||
namespace ConfigureRepo {
|
||||
namespace Parameters {
|
||||
export type Id = string;
|
||||
}
|
||||
export interface PathParameters {
|
||||
id: Parameters.Id;
|
||||
}
|
||||
export type RequestBody = Components.Schemas.RepositoryConfigInput;
|
||||
namespace Responses {
|
||||
export type $200 = Components.Schemas.RepositoryConfig;
|
||||
export type $400 = Components.Schemas.Error;
|
||||
export type $401 = Components.Schemas.Error;
|
||||
export type $404 = Components.Schemas.Error;
|
||||
export type $500 = Components.Schemas.Error;
|
||||
}
|
||||
}
|
||||
namespace ListAvailableRepos {
|
||||
namespace Responses {
|
||||
export type $200 = Components.Schemas.Repository[];
|
||||
export type $401 = Components.Schemas.Error;
|
||||
export type $403 = Components.Schemas.Error;
|
||||
export type $500 = Components.Schemas.Error;
|
||||
}
|
||||
}
|
||||
namespace ListConfiguredRepos {
|
||||
namespace Responses {
|
||||
export type $200 = Components.Schemas.RepositoryConfig[];
|
||||
export type $401 = Components.Schemas.Error;
|
||||
export type $500 = Components.Schemas.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface Operations {
|
||||
/**
|
||||
* GET /repos/available
|
||||
*/
|
||||
['listAvailableRepos']: {
|
||||
requestBody: any;
|
||||
params: UnknownParams;
|
||||
query: UnknownParams;
|
||||
headers: UnknownParams;
|
||||
cookies: UnknownParams;
|
||||
context: Context<any, UnknownParams, UnknownParams, UnknownParams, UnknownParams>;
|
||||
response: Paths.ListAvailableRepos.Responses.$200 | Paths.ListAvailableRepos.Responses.$401 | Paths.ListAvailableRepos.Responses.$403 | Paths.ListAvailableRepos.Responses.$500;
|
||||
}
|
||||
/**
|
||||
* GET /repos/configured
|
||||
*/
|
||||
['listConfiguredRepos']: {
|
||||
requestBody: any;
|
||||
params: UnknownParams;
|
||||
query: UnknownParams;
|
||||
headers: UnknownParams;
|
||||
cookies: UnknownParams;
|
||||
context: Context<any, UnknownParams, UnknownParams, UnknownParams, UnknownParams>;
|
||||
response: Paths.ListConfiguredRepos.Responses.$200 | Paths.ListConfiguredRepos.Responses.$401 | Paths.ListConfiguredRepos.Responses.$500;
|
||||
}
|
||||
/**
|
||||
* PUT /repos/{id}
|
||||
*/
|
||||
['configureRepo']: {
|
||||
requestBody: Paths.ConfigureRepo.RequestBody;
|
||||
params: Paths.ConfigureRepo.PathParameters;
|
||||
query: UnknownParams;
|
||||
headers: UnknownParams;
|
||||
cookies: UnknownParams;
|
||||
context: Context<Paths.ConfigureRepo.RequestBody, Paths.ConfigureRepo.PathParameters, UnknownParams, UnknownParams, UnknownParams>;
|
||||
response: Paths.ConfigureRepo.Responses.$200 | Paths.ConfigureRepo.Responses.$400 | Paths.ConfigureRepo.Responses.$401 | Paths.ConfigureRepo.Responses.$404 | Paths.ConfigureRepo.Responses.$500;
|
||||
}
|
||||
/**
|
||||
* GET /repos/available
|
||||
*/
|
||||
["listAvailableRepos"]: {
|
||||
requestBody: any;
|
||||
params: UnknownParams;
|
||||
query: UnknownParams;
|
||||
headers: UnknownParams;
|
||||
cookies: UnknownParams;
|
||||
context: Context<
|
||||
any,
|
||||
UnknownParams,
|
||||
UnknownParams,
|
||||
UnknownParams,
|
||||
UnknownParams
|
||||
>;
|
||||
response:
|
||||
| Paths.ListAvailableRepos.Responses.$200
|
||||
| Paths.ListAvailableRepos.Responses.$401
|
||||
| Paths.ListAvailableRepos.Responses.$403
|
||||
| Paths.ListAvailableRepos.Responses.$500;
|
||||
};
|
||||
/**
|
||||
* GET /repos/configured
|
||||
*/
|
||||
["listConfiguredRepos"]: {
|
||||
requestBody: any;
|
||||
params: UnknownParams;
|
||||
query: UnknownParams;
|
||||
headers: UnknownParams;
|
||||
cookies: UnknownParams;
|
||||
context: Context<
|
||||
any,
|
||||
UnknownParams,
|
||||
UnknownParams,
|
||||
UnknownParams,
|
||||
UnknownParams
|
||||
>;
|
||||
response:
|
||||
| Paths.ListConfiguredRepos.Responses.$200
|
||||
| Paths.ListConfiguredRepos.Responses.$401
|
||||
| Paths.ListConfiguredRepos.Responses.$500;
|
||||
};
|
||||
/**
|
||||
* PUT /repos/{id}
|
||||
*/
|
||||
["configureRepo"]: {
|
||||
requestBody: Paths.ConfigureRepo.RequestBody;
|
||||
params: Paths.ConfigureRepo.PathParameters;
|
||||
query: UnknownParams;
|
||||
headers: UnknownParams;
|
||||
cookies: UnknownParams;
|
||||
context: Context<
|
||||
Paths.ConfigureRepo.RequestBody,
|
||||
Paths.ConfigureRepo.PathParameters,
|
||||
UnknownParams,
|
||||
UnknownParams,
|
||||
UnknownParams
|
||||
>;
|
||||
response:
|
||||
| Paths.ConfigureRepo.Responses.$200
|
||||
| Paths.ConfigureRepo.Responses.$400
|
||||
| Paths.ConfigureRepo.Responses.$401
|
||||
| Paths.ConfigureRepo.Responses.$404
|
||||
| Paths.ConfigureRepo.Responses.$500;
|
||||
};
|
||||
}
|
||||
|
||||
export type OperationContext<operationId extends keyof Operations> = Operations[operationId]["context"];
|
||||
export type OperationResponse<operationId extends keyof Operations> = Operations[operationId]["response"];
|
||||
export type HandlerResponse<ResponseBody, ResponseModel = Record<string, any>> = ResponseModel & { _t?: ResponseBody };
|
||||
export type OperationHandlerResponse<operationId extends keyof Operations> = HandlerResponse<OperationResponse<operationId>>;
|
||||
export type OperationHandler<operationId extends keyof Operations, HandlerArgs extends unknown[] = unknown[]> = (...params: [OperationContext<operationId>, ...HandlerArgs]) => Promise<OperationHandlerResponse<operationId>>;
|
||||
|
||||
export type OperationContext<operationId extends keyof Operations> =
|
||||
Operations[operationId]["context"];
|
||||
export type OperationResponse<operationId extends keyof Operations> =
|
||||
Operations[operationId]["response"];
|
||||
export type HandlerResponse<
|
||||
ResponseBody,
|
||||
ResponseModel = Record<string, any>,
|
||||
> = ResponseModel & { _t?: ResponseBody };
|
||||
export type OperationHandlerResponse<operationId extends keyof Operations> =
|
||||
HandlerResponse<OperationResponse<operationId>>;
|
||||
export type OperationHandler<
|
||||
operationId extends keyof Operations,
|
||||
HandlerArgs extends unknown[] = unknown[],
|
||||
> = (
|
||||
...params: [OperationContext<operationId>, ...HandlerArgs]
|
||||
) => Promise<OperationHandlerResponse<operationId>>;
|
||||
|
||||
export type Error = Components.Schemas.Error;
|
||||
export type Repository = Components.Schemas.Repository;
|
||||
|
||||
8
server/src/types/user.ts
Normal file
8
server/src/types/user.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface User {
|
||||
id: number | string;
|
||||
login: string;
|
||||
email?: string;
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
// logic to insert user in db
|
||||
Loading…
x
Reference in New Issue
Block a user