diff --git a/lint_results.txt b/lint_results.txt new file mode 100644 index 0000000..bdac08d Binary files /dev/null and b/lint_results.txt differ diff --git a/lint_results_utf8.txt b/lint_results_utf8.txt new file mode 100644 index 0000000..8916d4a --- /dev/null +++ b/lint_results_utf8.txt @@ -0,0 +1,16 @@ + +> learnault-api@0.1.0 lint +> eslint . + + +C:\Users\EMMA\Desktop\learn\learnault-api\src\config\swagger.ts + 1:41 error Extra semicolon semi + 37:2 error Extra semicolon semi + 39:43 error Extra semicolon semi + +C:\Users\EMMA\Desktop\learn\learnault-api\src\controllers\user.controller.ts + 1:62 warning 'UpdateWalletData' is defined but never used. Allowed unused vars must match /^I[A-Z]|^_/u @typescript-eslint/no-unused-vars + +Γ£û 4 problems (3 errors, 1 warning) + 3 errors and 0 warnings potentially fixable with the `--fix` option. + diff --git a/package.json b/package.json index 23880e5..e626150 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,8 @@ "jsonwebtoken": "^9.0.2", "morgan": "^1.10.1", "pg": "^8.20.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "winston": "^3.19.0", "zod": "^3.25.76" }, @@ -59,6 +61,8 @@ "@types/node": "^22.19.13", "@types/pg": "^8.18.0", "@types/supertest": "^7.2.0", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", "@vitest/coverage-v8": "4.0.18", "eslint": "^10.0.2", "nodemon": "^3.1.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b7fc35..15912ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,12 @@ importers: pg: specifier: ^8.20.0 version: 8.20.0 + swagger-jsdoc: + specifier: ^6.2.8 + version: 6.2.8(openapi-types@12.1.3) + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1(express@5.2.1) winston: specifier: ^3.19.0 version: 3.19.0 @@ -78,6 +84,12 @@ importers: '@types/supertest': specifier: ^7.2.0 version: 7.2.0 + '@types/swagger-jsdoc': + specifier: ^6.0.4 + version: 6.0.4 + '@types/swagger-ui-express': + specifier: ^4.1.8 + version: 4.1.8 '@vitest/coverage-v8': specifier: 4.0.18 version: 4.0.18(vitest@4.0.18(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)) @@ -111,6 +123,21 @@ importers: packages: + '@apidevtools/json-schema-ref-parser@9.1.2': + resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} + + '@apidevtools/openapi-schemas@2.1.0': + resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} + engines: {node: '>=10'} + + '@apidevtools/swagger-methods@3.0.2': + resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} + + '@apidevtools/swagger-parser@10.0.3': + resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==} + peerDependencies: + openapi-types: '>=7' + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -411,6 +438,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@mrleebo/prisma-ast@0.13.1': resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} engines: {node: '>=16'} @@ -609,6 +639,9 @@ packages: cpu: [x64] os: [win32] + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@so-ric/colorspace@1.1.6': resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} @@ -729,6 +762,12 @@ packages: '@types/supertest@7.2.0': resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==} + '@types/swagger-jsdoc@6.0.4': + resolution: {integrity: sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==} + + '@types/swagger-ui-express@4.1.8': + resolution: {integrity: sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==} + '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} @@ -860,6 +899,9 @@ packages: arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -887,6 +929,9 @@ packages: axios@1.13.6: resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -917,6 +962,9 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@5.0.4: resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} engines: {node: 18 || 20 || >=22} @@ -955,6 +1003,9 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1006,9 +1057,20 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + commander@6.2.0: + resolution: {integrity: sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==} + engines: {node: '>= 6'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} @@ -1112,6 +1174,10 @@ packages: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -1336,6 +1402,9 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1376,6 +1445,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1452,6 +1525,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1522,6 +1599,10 @@ packages: js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1559,12 +1640,20 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} lodash.isboolean@3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + lodash.isinteger@4.0.4: resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} @@ -1577,6 +1666,9 @@ packages: lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -1777,6 +1869,9 @@ packages: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + morgan@1.10.1: resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} engines: {node: '>= 0.8.0'} @@ -1856,6 +1951,9 @@ packages: one-time@1.0.0: resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1876,6 +1974,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2193,6 +2295,24 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + swagger-jsdoc@6.2.8: + resolution: {integrity: sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==} + engines: {node: '>=12.0.0'} + hasBin: true + + swagger-parser@10.0.3: + resolution: {integrity: sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==} + engines: {node: '>=10'} + + swagger-ui-dist@5.32.0: + resolution: {integrity: sha512-nKZB0OuDvacB0s/lC2gbge+RigYvGRGpLLMWMFxaTUwfM+CfndVk9Th2IaTinqXiz6Mn26GK2zriCpv6/+5m3Q==} + + swagger-ui-express@5.0.1: + resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0 || >=5.0.0-beta' + text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} @@ -2325,6 +2445,10 @@ packages: typescript: optional: true + validator@13.15.26: + resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} + engines: {node: '>= 0.10'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2436,6 +2560,10 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + yaml@2.0.0-1: + resolution: {integrity: sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==} + engines: {node: '>= 6'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -2444,6 +2572,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + z-schema@5.0.5: + resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} + engines: {node: '>=8.0.0'} + hasBin: true + zeptomatch@2.1.0: resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} @@ -2455,6 +2588,27 @@ packages: snapshots: + '@apidevtools/json-schema-ref-parser@9.1.2': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + call-me-maybe: 1.0.2 + js-yaml: 4.1.1 + + '@apidevtools/openapi-schemas@2.1.0': {} + + '@apidevtools/swagger-methods@3.0.2': {} + + '@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.3)': + dependencies: + '@apidevtools/json-schema-ref-parser': 9.1.2 + '@apidevtools/openapi-schemas': 2.1.0 + '@apidevtools/swagger-methods': 3.0.2 + '@jsdevtools/ono': 7.1.3 + call-me-maybe: 1.0.2 + openapi-types: 12.1.3 + z-schema: 5.0.5 + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -2671,6 +2825,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jsdevtools/ono@7.1.3': {} + '@mrleebo/prisma-ast@0.13.1': dependencies: chevrotain: 10.5.0 @@ -2848,6 +3004,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true + '@scarf/scarf@1.4.0': {} + '@so-ric/colorspace@1.1.6': dependencies: color: 5.0.3 @@ -3004,6 +3162,13 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/swagger-jsdoc@6.0.4': {} + + '@types/swagger-ui-express@4.1.8': + dependencies: + '@types/express': 4.17.25 + '@types/serve-static': 1.15.10 + '@types/triple-beam@1.3.5': {} '@types/unist@3.0.3': {} @@ -3181,6 +3346,8 @@ snapshots: arg@4.1.3: {} + argparse@2.0.1: {} + asap@2.0.6: {} assertion-error@2.0.1: {} @@ -3209,6 +3376,8 @@ snapshots: transitivePeerDependencies: - debug + balanced-match@1.0.2: {} + balanced-match@4.0.4: {} base32.js@0.1.0: {} @@ -3239,6 +3408,11 @@ snapshots: transitivePeerDependencies: - supports-color + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + brace-expansion@5.0.4: dependencies: balanced-match: 4.0.4 @@ -3288,6 +3462,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + call-me-maybe@1.0.2: {} + ccount@2.0.1: {} chai@6.2.2: {} @@ -3346,8 +3522,15 @@ snapshots: commander@14.0.3: {} + commander@6.2.0: {} + + commander@9.5.0: + optional: true + component-emitter@1.3.1: {} + concat-map@0.0.1: {} + confbox@0.2.4: {} consola@3.4.2: {} @@ -3424,6 +3607,10 @@ snapshots: diff@4.0.4: {} + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -3702,6 +3889,8 @@ snapshots: fresh@2.0.0: {} + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true @@ -3754,6 +3943,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@7.1.6: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -3810,6 +4008,11 @@ snapshots: imurmurhash@0.1.4: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.4: {} ipaddr.js@1.9.1: {} @@ -3861,6 +4064,10 @@ snapshots: js-tokens@10.0.0: {} + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -3908,10 +4115,14 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.get@4.4.2: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} + lodash.isequal@4.5.0: {} + lodash.isinteger@4.0.4: {} lodash.isnumber@3.0.3: {} @@ -3920,6 +4131,8 @@ snapshots: lodash.isstring@4.0.1: {} + lodash.mergewith@4.6.2: {} + lodash.once@4.1.1: {} lodash@4.17.21: {} @@ -4294,6 +4507,10 @@ snapshots: dependencies: brace-expansion: 5.0.4 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + morgan@1.10.1: dependencies: basic-auth: 2.0.1 @@ -4379,6 +4596,8 @@ snapshots: dependencies: fn.name: 1.1.0 + openapi-types@12.1.3: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -4400,6 +4619,8 @@ snapshots: path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-to-regexp@8.3.0: {} @@ -4752,6 +4973,32 @@ snapshots: dependencies: has-flag: 4.0.0 + swagger-jsdoc@6.2.8(openapi-types@12.1.3): + dependencies: + commander: 6.2.0 + doctrine: 3.0.0 + glob: 7.1.6 + lodash.mergewith: 4.6.2 + swagger-parser: 10.0.3(openapi-types@12.1.3) + yaml: 2.0.0-1 + transitivePeerDependencies: + - openapi-types + + swagger-parser@10.0.3(openapi-types@12.1.3): + dependencies: + '@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.3) + transitivePeerDependencies: + - openapi-types + + swagger-ui-dist@5.32.0: + dependencies: + '@scarf/scarf': 1.4.0 + + swagger-ui-express@5.0.1(express@5.2.1): + dependencies: + express: 5.2.1 + swagger-ui-dist: 5.32.0 + text-hex@1.0.0: {} tinybench@2.9.0: {} @@ -4880,6 +5127,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 + validator@13.15.26: {} + vary@1.1.2: {} vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0): @@ -4978,10 +5227,20 @@ snapshots: xtend@4.0.2: {} + yaml@2.0.0-1: {} + yn@3.1.1: {} yocto-queue@0.1.0: {} + z-schema@5.0.5: + dependencies: + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + validator: 13.15.26 + optionalDependencies: + commander: 9.5.0 + zeptomatch@2.1.0: dependencies: grammex: 3.1.12 diff --git a/server_error.txt b/server_error.txt new file mode 100644 index 0000000..937985c Binary files /dev/null and b/server_error.txt differ diff --git a/server_error_2.txt b/server_error_2.txt new file mode 100644 index 0000000..6ef20b8 Binary files /dev/null and b/server_error_2.txt differ diff --git a/server_error_2_utf8.txt b/server_error_2_utf8.txt new file mode 100644 index 0000000..15f50c1 --- /dev/null +++ b/server_error_2_utf8.txt @@ -0,0 +1,86 @@ +node.exe : C:\Users\EMM +A\Desktop\learn\learnau +lt-api\node_modules\.pn +pm\@prisma+client@7.4.2 +_prisma_49b4b128965f74e +a9bbd7586bc0c7d7a\node_ +modules\@prisma\client\ +src\runtime\getPrismaCl +ient.ts:260 +At line:1 char:1 ++ & "C:\nvm4w\nodejs/no +de.exe" "C:\Users\EMMA\ +AppData\Roaming\npm/nod +e_ ... ++ ~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~ +~~ + + CategoryInfo + : NotSpecifi + ed: (C:\Users\EMMA + \D...maClient.ts:2 +60:String) [], Rem +oteException + + FullyQualifiedEr + rorId : NativeComm + andError + + throw new Prism +aClientInitializationEr +ror( + ^ + + +PrismaClientInitializat +ionError: +`PrismaClient` needs +to be constructed with +a non-empty, valid +`PrismaClientOptions`: + +``` +new PrismaClient({ + ... +}) +``` + +or + +``` +constructor() { + super({ ... }); +} +``` + + at new t (C:\Users\ +EMMA\Desktop\learn\lear +nault-api\node_modules\ +.pnpm\@prisma+client@7. +4.2_prisma_49b4b128965f +74ea9bbd7586bc0c7d7a\no +de_modules\@prisma\clie +nt\src\runtime\getPrism +aClient.ts:260:15) + at (C:\ +Users\EMMA\Desktop\lear +n\learnault-api\src\con +fig\database.ts:7:42) + at ModuleJob.run (n +ode:internal/modules/es +m/module_job:345:25) + at async onImport.t +racePromise.__proto__ ( +node:internal/modules/e +sm/loader:665:26) + at async asyncRunEn +tryPointWithESMLoader ( +node:internal/modules/r +un_main:117:5) { + clientVersion: +'7.4.2', + errorCode: undefined, + retryable: undefined +} + +Node.js v22.20.0 diff --git a/server_error_utf8.txt b/server_error_utf8.txt new file mode 100644 index 0000000..cbd4b5f --- /dev/null +++ b/server_error_utf8.txt @@ -0,0 +1,50 @@ +node.exe : C:\Users\EMM +A\Desktop\learn\learnau +lt-api\src\config\datab +ase.ts:1 +At line:1 char:1 ++ & "C:\nvm4w\nodejs/no +de.exe" "C:\Users\EMMA\ +AppData\Roaming\npm/nod +e_ ... ++ ~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~ +~~ + + CategoryInfo + : NotSpecifi + ed: (C:\Users\EMMA + \D...g\database.ts +:1:String) [], Rem +oteException + + FullyQualifiedEr + rorId : NativeComm + andError + +import { PrismaClient +} from '@prisma/client' + ^ + +SyntaxError: The +requested module +'@prisma/client' does +not provide an export +named 'PrismaClient' + at +ModuleJob._instantiate +(node:internal/modules/ +esm/module_job:228:21) + at async +ModuleJob.run (node:int +ernal/modules/esm/modul +e_job:337:5) + at async onImport.t +racePromise.__proto__ ( +node:internal/modules/e +sm/loader:665:26) + at async asyncRunEn +tryPointWithESMLoader ( +node:internal/modules/r +un_main:117:5) + +Node.js v22.20.0 diff --git a/src/app.ts b/src/app.ts index f79b234..09098da 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,23 +1,31 @@ +import dotenv from 'dotenv' +dotenv.config() + import express from 'express' import cors from 'cors' import helmet from 'helmet' import morgan from 'morgan' -import dotenv from 'dotenv' +import swaggerUi from 'swagger-ui-express' +import { specs } from './config/swagger' import routes from './routes' import { errorHandler, notFoundHandler } from './middleware/error.middleware' -dotenv.config() - const app: express.Application = express() app.use(express.json()) app.use(cors()) -app.use(helmet()) +app.use(helmet({ + contentSecurityPolicy: false, // Disable CSP for Swagger UI to work correctly +})) app.use(morgan('dev')) +// API routes app.use('/api', routes) +// Swagger documentation +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)) + // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }) diff --git a/src/config/database.ts b/src/config/database.ts index 719caf2..46685a3 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -4,7 +4,13 @@ const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined } -const prisma = globalForPrisma.prisma ?? new PrismaClient() +const prisma = globalForPrisma.prisma ?? new PrismaClient({ + datasources: { + db: { + url: process.env.DATABASE_URL, + }, + }, +}) if (process.env.NODE_ENV !== 'production') { globalForPrisma.prisma = prisma diff --git a/src/config/swagger.ts b/src/config/swagger.ts new file mode 100644 index 0000000..48b6b1b --- /dev/null +++ b/src/config/swagger.ts @@ -0,0 +1,41 @@ +import swaggerJsdoc from 'swagger-jsdoc' + + +const options: swaggerJsdoc.Options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Learnault API Documentation', + version: '1.0.0', + description: 'Comprehensive API documentation for Learnault - a decentralized learn-to-earn platform on Stellar', + contact: { + name: 'Learnault Contributors', + url: 'https://github.com/learnault/learnault', + }, + }, + servers: [ + { + url: '/api', + description: 'Main API base path', + }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + apis: ['./src/controllers/**/*.ts', './src/docs/*.ts'], // Path to the API docs +} + +export const specs = swaggerJsdoc(options) + diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index bea9947..31069a3 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -10,9 +10,30 @@ const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1d' export class AuthController { /** - * @route POST /api/v1/auth/register - * @desc Register a new user - * @access Public + * @openapi + * /auth/register: + * post: + * summary: Register a new user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RegisterInput' + * responses: + * 201: + * description: User registered successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AuthResponse' + * 400: + * description: Validation failed + * 409: + * description: User already exists + * 500: + * description: Internal server error */ async register(req: Request, res: Response): Promise { try { @@ -79,9 +100,30 @@ export class AuthController { } /** - * @route POST /api/v1/auth/login - * @desc Login a user - * @access Public + * @openapi + * /auth/login: + * post: + * summary: Login a user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginInput' + * responses: + * 200: + * description: Login successful + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AuthResponse' + * 400: + * description: Validation failed + * 401: + * description: Invalid credentials + * 500: + * description: Internal server error */ async login(req: Request, res: Response): Promise { try { @@ -143,9 +185,14 @@ export class AuthController { } /** - * @route POST /api/v1/auth/logout - * @desc Logout user (client-side usually handles this by deleting token, but can track server-side) - * @access Private (optional, here public) + * @openapi + * /auth/logout: + * post: + * summary: Logout user + * tags: [Auth] + * responses: + * 200: + * description: Logged out successfully */ async logout(req: Request, res: Response): Promise { // For stateless JWT, we can't truly "logout" unless we blacklist tokens. diff --git a/src/controllers/credential.controller.ts b/src/controllers/credential.controller.ts index 36f1c89..91f2ca3 100644 --- a/src/controllers/credential.controller.ts +++ b/src/controllers/credential.controller.ts @@ -5,9 +5,47 @@ import { prisma } from '../config/database' export class CredentialController { /** - * GET /credentials - * Retrieve all credentials for the authenticated user - * Query params: moduleId, fromDate, toDate, page, limit + * @openapi + * /credentials: + * get: + * summary: Retrieve user credentials + * tags: [Credentials] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: moduleId + * schema: + * type: string + * - in: query + * name: fromDate + * schema: + * type: string + * format: date + * - in: query + * name: toDate + * schema: + * type: string + * format: date + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * responses: + * 200: + * description: Credentials retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CredentialList' + * 401: + * description: Unauthorized */ getUserCredentials = asyncHandler( async (req: Request, res: Response): Promise => { @@ -76,7 +114,7 @@ export class CredentialController { res.json({ success: true, - data: credentials.map((cred) => ({ + data: credentials.map((cred: any) => ({ id: cred.id, userId: cred.userId, moduleId: cred.moduleId, @@ -100,9 +138,30 @@ export class CredentialController { ) /** - * GET /credentials/:id - * Retrieve a single credential by ID - * Requires authentication - user must own the credential + * @openapi + * /credentials/{id}: + * get: + * summary: Get credential by ID + * tags: [Credentials] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Credential details retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Credential' + * 401: + * description: Unauthorized + * 404: + * description: Credential not found */ getCredentialById = asyncHandler( async (req: Request, res: Response): Promise => { @@ -169,9 +228,26 @@ export class CredentialController { ) /** - * GET /credentials/verify/:onChainId - * Public endpoint to verify a credential - * No authentication required + * @openapi + * /credentials/verify/{onChainId}: + * get: + * summary: Verify a credential + * tags: [Credentials] + * parameters: + * - in: path + * name: onChainId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Credential verification status + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/VerificationResponse' + * 404: + * description: Credential not found */ verifyCredential = asyncHandler( async (req: Request, res: Response): Promise => { diff --git a/src/controllers/module.controller.ts b/src/controllers/module.controller.ts index 16c4870..e994877 100644 --- a/src/controllers/module.controller.ts +++ b/src/controllers/module.controller.ts @@ -1,334 +1,449 @@ -import { Request, Response } from 'express' -import { z } from 'zod' -import { prisma } from '../config/database' - -// Query parameter schemas for validation -const listModulesSchema = z.object({ - page: z.string().optional().transform(val => val ? parseInt(val) : 1), - limit: z.string().optional().transform(val => val ? parseInt(val) : 10), - category: z.string().optional(), - difficulty: z.string().optional(), - search: z.string().optional(), -}) - - -const completeModuleSchema = z.object({ - quizAnswers: z.array(z.object({ - questionId: z.string(), - answer: z.string(), - })), -}) - -// GET /modules - List modules with filters and pagination -export const listModules = async (req: Request, res: Response) => { - try { - const queryValidation = listModulesSchema.safeParse(req.query) - if (!queryValidation.success) { - return res.status(400).json({ - message: 'Invalid query parameters', - errors: queryValidation.error.errors - }) - } - - const { page, limit, category, difficulty, search } = queryValidation.data - const skip = (page - 1) * limit - - // Build where clause for filters - const where: any = {} - - if (category) { - where.category = category - } - - if (difficulty) { - where.difficulty = difficulty - } - - if (search) { - where.OR = [ - { title: { contains: search, mode: 'insensitive' } }, - { description: { contains: search, mode: 'insensitive' } } - ] - } - - // Get total count for pagination - const total = await prisma.module.count({ where }) - - // Get modules with pagination - const modules = await prisma.module.findMany({ - where, - skip, - take: limit, - orderBy: { createdAt: 'desc' }, - include: { - _count: { - select: { - completions: true - } - } - } - }) - - // If user is authenticated, include their progress - const userProgress: any = {} - if (req.user) { - const userCompletions = await prisma.completion.findMany({ - where: { userId: req.user.id }, - select: { moduleId: true, score: true, completedAt: true } - }) - - userCompletions.forEach((completion: any) => { - userProgress[completion.moduleId] = { - completed: true, - score: completion.score, - completedAt: completion.completedAt - } - }) - } - - // Transform response - const transformedModules = modules.map((module: any) => ({ - id: module.id, - title: module.title, - description: module.description, - category: module.category, - difficulty: module.difficulty, - reward: module.reward, - createdAt: module.createdAt, - updatedAt: module.updatedAt, - completionCount: module._count.completions, - userProgress: userProgress[module.id] || null - })) - - res.json({ - modules: transformedModules, - pagination: { - page, - limit, - total, - totalPages: Math.ceil(total / limit), - hasNext: page * limit < total, - hasPrev: page > 1 - } - }) - - } catch (error) { - console.error('Error listing modules:', error) - res.status(500).json({ message: 'Internal server error' }) - } -} - -// GET /modules/:id - Get module details -export const getModuleById = async (req: Request, res: Response) => { - try { - const { id } = req.params - - const module = await prisma.module.findUnique({ - where: { id }, - include: { - _count: { - select: { - completions: true - } - } - } - }) - - if (!module) { - return res.status(404).json({ message: 'Module not found' }) - } - - // Get user's progress if authenticated - let userProgress = null - if (req.user) { - const completion = await prisma.completion.findUnique({ - where: { - userId_moduleId: { - userId: req.user.id, - moduleId: id - } - } - }) - - if (completion) { - userProgress = { - completed: true, - score: completion.score, - completedAt: completion.completedAt - } - } - } - - const response = { - id: module.id, - title: module.title, - description: module.description, - category: module.category, - difficulty: module.difficulty, - reward: module.reward, - createdAt: module.createdAt, - updatedAt: module.updatedAt, - completionCount: module._count.completions, - userProgress - } - - res.json(response) - - } catch (error) { - console.error('Error getting module:', error) - res.status(500).json({ message: 'Internal server error' }) - } -} - -// POST /modules/:id/start - Start tracking progress -export const startModule = async (req: Request, res: Response) => { - try { - if (!req.user) { - return res.status(401).json({ message: 'Authentication required' }) - } - - const { id } = req.params - - // Check if module exists - const module = await prisma.module.findUnique({ - where: { id } - }) - - if (!module) { - return res.status(404).json({ message: 'Module not found' }) - } - - // Check if user already has a completion record - const existingCompletion = await prisma.completion.findUnique({ - where: { - userId_moduleId: { - userId: req.user.id, - moduleId: id - } - } - }) - - if (existingCompletion) { - return res.status(400).json({ - message: 'Module already started or completed', - status: existingCompletion.score !== null ? 'completed' : 'in_progress' - }) - } - - // Create completion record with null score (in progress) - const completion = await prisma.completion.create({ - data: { - userId: req.user.id, - moduleId: id, - score: null // null indicates in progress - } - }) - - res.status(201).json({ - message: 'Module started successfully', - completionId: completion.id, - startedAt: completion.createdAt - }) - - } catch (error) { - console.error('Error starting module:', error) - res.status(500).json({ message: 'Internal server error' }) - } -} - -// POST /modules/:id/complete - Complete module with quiz answers -export const completeModule = async (req: Request, res: Response) => { - try { - if (!req.user) { - return res.status(401).json({ message: 'Authentication required' }) - } - - const { id } = req.params - const bodyValidation = completeModuleSchema.safeParse(req.body) - - if (!bodyValidation.success) { - return res.status(400).json({ - message: 'Invalid request body', - errors: bodyValidation.error.errors - }) - } - - const { quizAnswers } = bodyValidation.data - - // Check if module exists - const module = await prisma.module.findUnique({ - where: { id } - }) - - if (!module) { - return res.status(404).json({ message: 'Module not found' }) - } - - // Check if user has started this module - const completion = await prisma.completion.findUnique({ - where: { - userId_moduleId: { - userId: req.user.id, - moduleId: id - } - } - }) - - if (!completion) { - return res.status(400).json({ message: 'Module must be started before completion' }) - } - - if (completion.score !== null) { - return res.status(400).json({ message: 'Module already completed' }) - } - - // Calculate score (simplified - in real implementation, this would validate against actual quiz questions) - // For now, we'll simulate a scoring mechanism - const correctAnswers = quizAnswers.length // Simplified: assume all answers are correct - const totalQuestions = quizAnswers.length || 1 // Avoid division by zero - const score = Math.round((correctAnswers / totalQuestions) * 100) - - // Update completion record - const updatedCompletion = await prisma.completion.update({ - where: { - userId_moduleId: { - userId: req.user.id, - moduleId: id - } - }, - data: { - score, - completedAt: new Date() - } - }) - - // Check reward eligibility (score >= 70%) - const isEligibleForReward = score >= 70 - let rewardTransaction = null - - if (isEligibleForReward) { - // Create reward transaction - rewardTransaction = await prisma.transaction.create({ - data: { - userId: req.user.id, - amount: module.reward, - type: 'reward', - status: 'pending' - } - }) - } - - res.json({ - message: 'Module completed successfully', - score, - isEligibleForReward, - reward: isEligibleForReward ? module.reward : 0, - rewardTransaction: rewardTransaction?.id, - completedAt: updatedCompletion.completedAt - }) - - } catch (error) { - console.error('Error completing module:', error) - res.status(500).json({ message: 'Internal server error' }) - } -} \ No newline at end of file +import { Request, Response } from 'express' +import { z } from 'zod' +import { prisma } from '../config/database' + +// Query parameter schemas for validation +const listModulesSchema = z.object({ + page: z.string().optional().transform(val => val ? parseInt(val) : 1), + limit: z.string().optional().transform(val => val ? parseInt(val) : 10), + category: z.string().optional(), + difficulty: z.string().optional(), + search: z.string().optional(), +}) + + +const completeModuleSchema = z.object({ + quizAnswers: z.array(z.object({ + questionId: z.string(), + answer: z.string(), + })), +}) + +/** + * @openapi + * /modules: + * get: + * summary: List modules with filters and pagination + * tags: [Modules] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * - in: query + * name: category + * schema: + * type: string + * - in: query + * name: difficulty + * schema: + * type: string + * - in: query + * name: search + * schema: + * type: string + * responses: + * 200: + * description: List of modules retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ModuleList' + * 400: + * description: Invalid query parameters + */ +export const listModules = async (req: Request, res: Response) => { + try { + const queryValidation = listModulesSchema.safeParse(req.query) + if (!queryValidation.success) { + return res.status(400).json({ + message: 'Invalid query parameters', + errors: queryValidation.error.errors + }) + } + + const { page, limit, category, difficulty, search } = queryValidation.data + const skip = (page - 1) * limit + + // Build where clause for filters + const where: any = {} + + if (category) { + where.category = category + } + + if (difficulty) { + where.difficulty = difficulty + } + + if (search) { + where.OR = [ + { title: { contains: search, mode: 'insensitive' } }, + { description: { contains: search, mode: 'insensitive' } } + ] + } + + // Get total count for pagination + const total = await prisma.module.count({ where }) + + // Get modules with pagination + const modules = await prisma.module.findMany({ + where, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { + _count: { + select: { + completions: true + } + } + } + }) + + // If user is authenticated, include their progress + const userProgress: any = {} + if (req.user) { + const userCompletions = await prisma.completion.findMany({ + where: { userId: req.user.id }, + select: { moduleId: true, score: true, completedAt: true } + }) + + userCompletions.forEach((completion: any) => { + userProgress[completion.moduleId] = { + completed: true, + score: completion.score, + completedAt: completion.completedAt + } + }) + } + + // Transform response + const transformedModules = modules.map((module: any) => ({ + id: module.id, + title: module.title, + description: module.description, + category: module.category, + difficulty: module.difficulty, + reward: module.reward, + createdAt: module.createdAt, + updatedAt: module.updatedAt, + completionCount: module._count.completions, + userProgress: userProgress[module.id] || null + })) + + res.json({ + modules: transformedModules, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + hasNext: page * limit < total, + hasPrev: page > 1 + } + }) + + } catch (error) { + console.error('Error listing modules:', error) + res.status(500).json({ message: 'Internal server error' }) + } +} + +/** + * @openapi + * /modules/{id}: + * get: + * summary: Get module details + * tags: [Modules] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Module details retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Module' + * 404: + * description: Module not found + */ +export const getModuleById = async (req: Request, res: Response) => { + try { + const { id } = req.params + + const module = await prisma.module.findUnique({ + where: { id }, + include: { + _count: { + select: { + completions: true + } + } + } + }) + + if (!module) { + return res.status(404).json({ message: 'Module not found' }) + } + + // Get user's progress if authenticated + let userProgress = null + if (req.user) { + const completion = await prisma.completion.findUnique({ + where: { + userId_moduleId: { + userId: req.user.id, + moduleId: id + } + } + }) + + if (completion) { + userProgress = { + completed: true, + score: completion.score, + completedAt: completion.completedAt + } + } + } + + const response = { + id: module.id, + title: module.title, + description: module.description, + category: module.category, + difficulty: module.difficulty, + reward: module.reward, + createdAt: module.createdAt, + updatedAt: module.updatedAt, + completionCount: module._count.completions, + userProgress + } + + res.json(response) + + } catch (error) { + console.error('Error getting module:', error) + res.status(500).json({ message: 'Internal server error' }) + } +} + +/** + * @openapi + * /modules/{id}/start: + * post: + * summary: Start a module + * tags: [Modules] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 201: + * description: Module started successfully + * 400: + * description: Module already started or completed + * 401: + * description: Unauthorized + * 404: + * description: Module not found + */ +export const startModule = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ message: 'Authentication required' }) + } + + const { id } = req.params + + // Check if module exists + const module = await prisma.module.findUnique({ + where: { id } + }) + + if (!module) { + return res.status(404).json({ message: 'Module not found' }) + } + + // Check if user already has a completion record + const existingCompletion = await prisma.completion.findUnique({ + where: { + userId_moduleId: { + userId: req.user.id, + moduleId: id + } + } + }) + + if (existingCompletion) { + return res.status(400).json({ + message: 'Module already started or completed', + status: existingCompletion.score !== null ? 'completed' : 'in_progress' + }) + } + + // Create completion record with null score (in progress) + const completion = await prisma.completion.create({ + data: { + userId: req.user.id, + moduleId: id, + score: null // null indicates in progress + } + }) + + res.status(201).json({ + message: 'Module started successfully', + completionId: completion.id, + startedAt: completion.createdAt + }) + + } catch (error) { + console.error('Error starting module:', error) + res.status(500).json({ message: 'Internal server error' }) + } +} + +/** + * @openapi + * /modules/{id}/complete: + * post: + * summary: Complete a module with quiz answers + * tags: [Modules] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CompleteModuleInput' + * responses: + * 200: + * description: Module completed successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ModuleCompletionResponse' + * 400: + * description: Invalid request or module already completed + * 401: + * description: Unauthorized + * 404: + * description: Module not found + */ +export const completeModule = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ message: 'Authentication required' }) + } + + const { id } = req.params + const bodyValidation = completeModuleSchema.safeParse(req.body) + + if (!bodyValidation.success) { + return res.status(400).json({ + message: 'Invalid request body', + errors: bodyValidation.error.errors + }) + } + + const { quizAnswers } = bodyValidation.data + + // Check if module exists + const module = await prisma.module.findUnique({ + where: { id } + }) + + if (!module) { + return res.status(404).json({ message: 'Module not found' }) + } + + // Check if user has started this module + const completion = await prisma.completion.findUnique({ + where: { + userId_moduleId: { + userId: req.user.id, + moduleId: id + } + } + }) + + if (!completion) { + return res.status(400).json({ message: 'Module must be started before completion' }) + } + + if (completion.score !== null) { + return res.status(400).json({ message: 'Module already completed' }) + } + + // Calculate score (simplified - in real implementation, this would validate against actual quiz questions) + // For now, we'll simulate a scoring mechanism + const correctAnswers = quizAnswers.length // Simplified: assume all answers are correct + const totalQuestions = quizAnswers.length || 1 // Avoid division by zero + const score = Math.round((correctAnswers / totalQuestions) * 100) + + // Update completion record + const updatedCompletion = await prisma.completion.update({ + where: { + userId_moduleId: { + userId: req.user.id, + moduleId: id + } + }, + data: { + score, + completedAt: new Date() + } + }) + + // Check reward eligibility (score >= 70%) + const isEligibleForReward = score >= 70 + let rewardTransaction = null + + if (isEligibleForReward) { + // Create reward transaction + rewardTransaction = await prisma.transaction.create({ + data: { + userId: req.user.id, + amount: module.reward, + type: 'reward', + status: 'pending' + } + }) + } + + res.json({ + message: 'Module completed successfully', + score, + isEligibleForReward, + reward: isEligibleForReward ? module.reward : 0, + rewardTransaction: rewardTransaction?.id, + completedAt: updatedCompletion.completedAt + }) + + } catch (error) { + console.error('Error completing module:', error) + res.status(500).json({ message: 'Internal server error' }) + } +} \ No newline at end of file diff --git a/src/controllers/reward.controller.ts b/src/controllers/reward.controller.ts index 7530ce7..fdaaae4 100644 --- a/src/controllers/reward.controller.ts +++ b/src/controllers/reward.controller.ts @@ -11,12 +11,26 @@ export class RewardController { } /** - * GET /rewards/balance - * Retrieve the current user's reward balance + * @openapi + * /rewards/balance: + * get: + * summary: Get current user reward balance + * tags: [Rewards] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Reward balance retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RewardBalance' + * 401: + * description: Unauthorized */ getBalance = asyncHandler( async (req: Request, res: Response): Promise => { - const userId = (req as any).user.id + const userId = (req as any).user?.id if (!userId) { throw new UnauthorizedError('User ID not found') @@ -39,13 +53,57 @@ export class RewardController { ) /** - * GET /rewards/history - * Retrieve transaction history with optional filtering and pagination - * Query params: type, status, fromDate, toDate, limit, offset + * @openapi + * /rewards/history: + * get: + * summary: Get transaction history + * tags: [Rewards] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: type + * schema: + * type: string + * enum: [module_reward, streak_bonus, referral_reward, withdrawal] + * - in: query + * name: status + * schema: + * type: string + * enum: [pending, completed, failed] + * - in: query + * name: fromDate + * schema: + * type: string + * format: date-time + * - in: query + * name: toDate + * schema: + * type: string + * format: date-time + * - in: query + * name: limit + * schema: + * type: integer + * default: 20 + * - in: query + * name: offset + * schema: + * type: integer + * default: 0 + * responses: + * 200: + * description: Transaction history retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/TransactionHistory' + * 401: + * description: Unauthorized */ getHistory = asyncHandler( async (req: Request, res: Response): Promise => { - const userId = (req as any).user.id + const userId = (req as any).user?.id if (!userId) { throw new UnauthorizedError('User ID not found') @@ -138,13 +196,34 @@ export class RewardController { ) /** - * POST /rewards/withdraw - * Process a withdrawal request - * Body: walletAddress, amount, memo (optional) + * @openapi + * /rewards/withdraw: + * post: + * summary: Process withdrawal request + * tags: [Rewards] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/WithdrawalInput' + * responses: + * 201: + * description: Withdrawal processed successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/WithdrawalResponse' + * 400: + * description: Invalid input or insufficient balance + * 401: + * description: Unauthorized */ withdraw = asyncHandler( async (req: Request, res: Response): Promise => { - const userId = (req as any).user.id + const userId = (req as any).user?.id if (!userId) { throw new UnauthorizedError('User ID not found') diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 6c5e3b0..892f625 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -1,18 +1,42 @@ -import { ChangePasswordData, PublicUserInfo, UpdateUserData, UpdateWalletData, User } from '../types/user.types' +import { ChangePasswordData, PublicUserInfo, UpdateUserData, User } from '../types/user.types' import { Request, Response } from 'express' export class UserController { + /** + * @openapi + * /users/me: + * get: + * summary: Get current authenticated user profile + * tags: [Users] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: User profile retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * 401: + * description: Unauthorized + * 404: + * description: User not found + */ + async getCurrentUser (req: Request, res: Response): Promise { try { - const userId = (req as any).user.id - + const userId = (req as any).user?.id + if (!userId) { + res.status(401).json({ error: 'Unauthorized' }) + + return + } const user = await this.findUserById(userId) if (!user) { res.status(404).json({ error: 'User not found' }) - + return } - res.json({ id: user.id, email: user.email, @@ -26,34 +50,62 @@ export class UserController { createdAt: user.createdAt, updatedAt: user.updatedAt, }) - } catch (error) { - console.error('Error getting current user:', error) + } catch { res.status(500).json({ error: 'Internal server error' }) } } + /** + * @openapi + * /users/profile: + * put: + * summary: Update user profile + * tags: [Users] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/UpdateUser' + * responses: + * 200: + * description: Profile updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * 401: + * description: Unauthorized + * 500: + * description: Internal server error + */ + async updateProfile (req: Request, res: Response): Promise { try { - const userId = (req as any).user.id - const updateData: UpdateUserData = req.body - - const updatedUser = await this.updateUserProfile(userId, updateData) - + const userId = (req as any).user?.id + if (!userId) { + res.status(401).json({ error: 'Unauthorized' }) + + return + } + const data = req.body as UpdateUserData + const user = await this.updateUserProfile(userId, data) res.json({ - id: updatedUser.id, - email: updatedUser.email, - username: updatedUser.username, - firstName: updatedUser.firstName, - lastName: updatedUser.lastName, - bio: updatedUser.bio, - avatar: updatedUser.avatar, - walletAddress: updatedUser.walletAddress, - isActive: updatedUser.isActive, - createdAt: updatedUser.createdAt, - updatedAt: updatedUser.updatedAt, + id: user.id, + email: user.email, + username: user.username, + firstName: user.firstName, + lastName: user.lastName, + bio: user.bio, + avatar: user.avatar, + walletAddress: user.walletAddress, + isActive: user.isActive, + createdAt: user.createdAt, + updatedAt: user.updatedAt, }) - } catch (error) { - console.error('Error updating profile:', error) + } catch { res.status(500).json({ error: 'Internal server error' }) } } @@ -75,6 +127,7 @@ export class UserController { firstName: user.firstName, lastName: user.lastName, avatar: user.avatar, + role: user.role, createdAt: user.createdAt, } @@ -87,13 +140,14 @@ export class UserController { async changePassword (req: Request, res: Response): Promise { try { - const userId = (req as any).user.id + const userId = (req as any).user?.id const { currentPassword, newPassword }: ChangePasswordData = req.body const user = await this.findUserById(userId) if (!user) { res.status(404).json({ error: 'User not found' }) + return } @@ -101,46 +155,83 @@ export class UserController { if (!isCurrentPasswordValid) { res.status(400).json({ error: 'Current password is incorrect' }) + return } await this.updateUserPassword(userId, newPassword) res.json({ message: 'Password updated successfully' }) - } catch (error) { + } catch { console.error('Error changing password:', error) res.status(500).json({ error: 'Internal server error' }) } } + /** + * @openapi + * /users/wallet: + * put: + * summary: Update user Stellar wallet address + * tags: [Users] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - walletAddress + * properties: + * walletAddress: + * type: string + * example: GABC123456789012345678901234567890123456789012345678901234567890 + * responses: + * 200: + * description: Wallet address updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/User' + * 400: + * description: Invalid Stellar wallet address + * 401: + * description: Unauthorized + * 500: + * description: Internal server error + */ + async updateWalletAddress (req: Request, res: Response): Promise { try { - const userId = (req as any).user.id - const { walletAddress }: UpdateWalletData = req.body - + const userId = (req as any).user?.id + if (!userId) { + res.status(401).json({ error: 'Unauthorized' }) + + return + } + const { walletAddress } = req.body as { walletAddress: string } if (!this.isValidStellarAddress(walletAddress)) { res.status(400).json({ error: 'Invalid Stellar wallet address' }) - + return } - - const updatedUser = await this.updateUserWallet(userId, walletAddress) - + const user = await this.updateUserWallet(userId, walletAddress) res.json({ - id: updatedUser.id, - email: updatedUser.email, - username: updatedUser.username, - firstName: updatedUser.firstName, - lastName: updatedUser.lastName, - bio: updatedUser.bio, - avatar: updatedUser.avatar, - walletAddress: updatedUser.walletAddress, - isActive: updatedUser.isActive, - createdAt: updatedUser.createdAt, - updatedAt: updatedUser.updatedAt, + id: user.id, + email: user.email, + username: user.username, + firstName: (user as any).firstName, + lastName: (user as any).lastName, + bio: (user as any).bio, + avatar: (user as any).avatar, + walletAddress: user.walletAddress, + isActive: user.isActive, + createdAt: user.createdAt, + updatedAt: user.updatedAt, }) - } catch (error) { - console.error('Error updating wallet address:', error) + } catch { res.status(500).json({ error: 'Internal server error' }) } } @@ -156,6 +247,8 @@ export class UserController { avatar: 'https://example.com/avatar.jpg', walletAddress: 'GABC123456789012345678901234567890123456789012345678901234567890', isActive: true, + role: 'LEARNER' as any, + status: 'active' as any, createdAt: new Date(), updatedAt: new Date(), } @@ -174,6 +267,8 @@ export class UserController { avatar: data.avatar, walletAddress: 'GABC123456789012345678901234567890123456789012345678901234567890', isActive: true, + role: 'LEARNER' as any, + status: 'active' as any, createdAt: new Date(), updatedAt: new Date(), } diff --git a/src/docs/schemas.ts b/src/docs/schemas.ts new file mode 100644 index 0000000..8f9e537 --- /dev/null +++ b/src/docs/schemas.ts @@ -0,0 +1,357 @@ +/** + * @openapi + * components: + * schemas: + * User: + * type: object + * properties: + * id: + * type: string + * format: uuid + * email: + * type: string + * format: email + * username: + * type: string + * firstName: + * type: string + * lastName: + * type: string + * bio: + * type: string + * avatar: + * type: string + * format: url + * walletAddress: + * type: string + * isActive: + * type: boolean + * role: + * type: string + * enum: [LEARNER, EMPLOYER, ADMIN] + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * + * UpdateUser: + * type: object + * properties: + * username: + * type: string + * firstName: + * type: string + * lastName: + * type: string + * bio: + * type: string + * avatar: + * type: string + * format: url + * + * RegisterInput: + * type: object + * required: + * - email + * - password + * - username + * properties: + * email: + * type: string + * format: email + * password: + * type: string + * format: password + * username: + * type: string + * role: + * type: string + * enum: [LEARNER, EMPLOYER] + * + * LoginInput: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * format: email + * password: + * type: string + * format: password + * + * AuthResponse: + * type: object + * properties: + * message: + * type: string + * token: + * type: string + * user: + * $ref: '#/components/schemas/User' + * + * Module: + * type: object + * properties: + * id: + * type: string + * format: uuid + * title: + * type: string + * description: + * type: string + * category: + * type: string + * difficulty: + * type: string + * reward: + * type: number + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * completionCount: + * type: integer + * userProgress: + * type: object + * nullable: true + * properties: + * completed: + * type: boolean + * score: + * type: number + * completedAt: + * type: string + * format: date-time + * + * ModuleList: + * type: object + * properties: + * modules: + * type: array + * items: + * $ref: '#/components/schemas/Module' + * pagination: + * $ref: '#/components/schemas/Pagination' + * + * Pagination: + * type: object + * properties: + * page: + * type: integer + * limit: + * type: integer + * total: + * type: integer + * totalPages: + * type: integer + * hasNext: + * type: boolean + * hasPrev: + * type: boolean + * + * CompleteModuleInput: + * type: object + * required: + * - quizAnswers + * properties: + * quizAnswers: + * type: array + * items: + * type: object + * properties: + * questionId: + * type: string + * answer: + * type: string + * + * ModuleCompletionResponse: + * type: object + * properties: + * message: + * type: string + * score: + * type: number + * isEligibleForReward: + * type: boolean + * reward: + * type: number + * rewardTransaction: + * type: string + * completedAt: + * type: string + * format: date-time + * + * RewardBalance: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * balance: + * type: object + * properties: + * available: + * type: number + * pending: + * type: number + * lifetime: + * type: number + * updatedAt: + * type: string + * format: date-time + * + * Transaction: + * type: object + * properties: + * id: + * type: string + * type: + * type: string + * status: + * type: string + * amount: + * type: number + * moduleId: + * type: string + * stellarTxHash: + * type: string + * createdAt: + * type: string + * format: date-time + * completedAt: + * type: string + * format: date-time + * + * TransactionHistory: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * transactions: + * type: array + * items: + * $ref: '#/components/schemas/Transaction' + * pagination: + * type: object + * properties: + * total: + * type: integer + * limit: + * type: integer + * offset: + * type: integer + * hasMore: + * type: boolean + * + * WithdrawalInput: + * type: object + * required: + * - walletAddress + * - amount + * properties: + * walletAddress: + * type: string + * amount: + * type: number + * memo: + * type: string + * + * WithdrawalResponse: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * transactionId: + * type: string + * amount: + * type: number + * stellarTxHash: + * type: string + * status: + * type: string + * requestedAt: + * type: string + * format: date-time + * completedAt: + * type: string + * format: date-time + * + * Credential: + * type: object + * properties: + * id: + * type: string + * userId: + * type: string + * holderName: + * type: string + * moduleId: + * type: string + * moduleName: + * type: string + * moduleDescription: + * type: string + * moduleCategory: + * type: string + * moduleDifficulty: + * type: string + * onChainId: + * type: string + * issuedAt: + * type: string + * format: date-time + * shareableLink: + * type: string + * + * CredentialList: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * $ref: '#/components/schemas/Credential' + * meta: + * type: object + * properties: + * page: + * type: integer + * limit: + * type: integer + * total: + * type: integer + * totalPages: + * type: integer + * + * VerificationResponse: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * valid: + * type: boolean + * credential: + * type: object + * verification: + * type: object + */ diff --git a/swagger-server.ts b/swagger-server.ts new file mode 100644 index 0000000..b4aaa51 --- /dev/null +++ b/swagger-server.ts @@ -0,0 +1,59 @@ +import express from 'express' +import swaggerUi from 'swagger-ui-express' +import swaggerJsdoc from 'swagger-jsdoc' +import cors from 'cors' + +const app = express() +const PORT = 5000 + +app.use(cors()) + +// Use the same configuration as the main app +const options: swaggerJsdoc.Options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Learnault API Documentation', + version: '1.0.0', + description: 'Comprehensive API documentation for Learnault - a decentralized learn-to-earn platform on Stellar', + contact: { + name: 'Learnault Contributors', + url: 'https://github.com/learnault/learnault', + }, + }, + servers: [ + { + url: '/api', + description: 'Main API base path', + }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + apis: ['./src/controllers/**/*.ts', './src/docs/*.ts'], +} + +const specs = swaggerJsdoc(options) + +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)) + +app.get('/', (req, res) => { + res.redirect('/api-docs') +}) + +app.listen(PORT, () => { + console.log(`Swagger documentation server running on port ${PORT}`) + console.log(`View at http://localhost:${PORT}/api-docs`) +})