From 4c4ddd989245602b34322e4d27da7c4b0278f558 Mon Sep 17 00:00:00 2001 From: Emmzyemms Date: Tue, 10 Mar 2026 00:47:59 +0100 Subject: [PATCH 1/4] feat/API Documentation --- lint_results.txt | Bin 0 -> 1134 bytes lint_results_utf8.txt | 16 + package.json | 4 + pnpm-lock.yaml | 259 ++++++++ server_error.txt | Bin 0 -> 2014 bytes server_error_2.txt | Bin 0 -> 3188 bytes server_error_2_utf8.txt | 86 +++ server_error_utf8.txt | 50 ++ src/app.ts | 16 +- src/config/database.ts | 8 +- src/config/swagger.ts | 41 ++ src/controllers/auth.controller.ts | 65 +- src/controllers/credential.controller.ts | 96 ++- src/controllers/module.controller.ts | 783 +++++++++++++---------- src/controllers/reward.controller.ts | 95 ++- src/controllers/user.controller.ts | 174 ++--- src/docs/schemas.ts | 357 +++++++++++ swagger-server.ts | 61 ++ 18 files changed, 1658 insertions(+), 453 deletions(-) create mode 100644 lint_results.txt create mode 100644 lint_results_utf8.txt create mode 100644 server_error.txt create mode 100644 server_error_2.txt create mode 100644 server_error_2_utf8.txt create mode 100644 server_error_utf8.txt create mode 100644 src/config/swagger.ts create mode 100644 src/docs/schemas.ts create mode 100644 swagger-server.ts diff --git a/lint_results.txt b/lint_results.txt new file mode 100644 index 0000000000000000000000000000000000000000..bdac08d3cf830d9f95b1057fe2350af7974c4660 GIT binary patch literal 1134 zcmcJP%SyyR5JfMz6a0tXso?lV(S<1L!i7r_bbQXlG3Y!sEDjRo>qwG_DN=-73+AGWB>5(F!$?21>Q5dCh8pH6L*)m_n6bq8xN-3t()o z*0$Ew)B%VD<{Wuf`#f7nJJ@pFz|<#7vlhQD|IDR{Po@_57JfZl5xIkw>z-^oJdQLA zYaX)*WE{wSUCX>J`+J6O7PVbHfX(m?cqHn=XXe|;ZhU3+nXE5q6}H7F{{`Dx%xx+1 z{U$f(_y6^Fty~>v=Je-SpyU}W< zZj9N|HS(NJwWniP8amR6p3u**4*3?Ck3x@R?tX5;lOlV1>XR`?ygs&i+{RbjYF?hl zUi1p%GHU=+pL4m=-I&`Hy;twJTr=qI>d{(Ge|7#(eBeyIST9fVsFzPaAfjvU`A)&Q lW{=oCWe@0OHug+GxH5hx?B|@V@rgS}_e`6^Gy45c_yEkvz=Z$+ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..937985c55d857487f1add0c27e1f7a02fb7fbd66 GIT binary patch literal 2014 zcmbW2TW`}q5QXO%iT_~rfwThB8!aeLRBe!gNChc~2MUp!IFOP!Au*5$!A}RiGixih zqS8{WZ0}_*=bW8g|M{KT#QK(4ooB%lS%c@8_qn~XIWsff=hn0~?XKOiBSt68&+W5i zcuyIR7{^$qm40kr-D8B^L)ND@WPOfZk8cm()CSCbWYlLpk_%{O%a6h*NvMzZf-5h+k40}%9~IgJIe%K6LxKBE!Yj{ z7dDaST8)aHYUmWZ%+63FVV{P+(^@ZbBxoA2!+R|Rpi@hRM#p2T> z*NS1pNV>IH3Fqq6^&C=_39Kg|2pEbSG5JE2;2Gxi9%4E5XgUQycwRcMIrA~GbS@IS zwJQ6cdrC5)_+;_zatLv~t*iSr0h^lidR>D6)xgqO{C9NWQi;^xvk+L$*v)8UvsmfV* zPsr9T;eAg3f|~1+Gj&eNsk)GT!Do}XaECVBPAj?zRT9Z9b3qMHNJIh!DEGQ-}db>_ztc`r_dHj#UiF*WvN5qM}US=RZq#d}jhu30XsoZfb zn_v(U-6l(rPx8Hf%Qxc;G&v(}dr``F8|*XBp}p&(OYAvxkL^|z7Y^O7;oqhIS;VtcIk)<=)PosW)t?4#G$=%*_V!%j4l*~BWCOk>;daJn37dy zckCR>36cu&mVcD}X|?~&bu;y$ZA+qI{lcHkg8GN%|}{XRSQ*&iX{ zE@R*SWer`0_)y=beuIX71D8>v|2cf-ctToF;E}U_#(QDE)T}$iHpxi#b=fb8LJz&} z`pyA=I_`l2bRWAq@P6)o*GlL+{QHb?tBPP@S4iBk>zXYu;JfWn5TYLYTQ%f*>=kz* z4)FAh49L(&J~}0e8B!+jeCBc#y^t_=L~cVkcl}9P{%vg^A==Ug-Nz$@>{O3@dFWQ= z*fk|qOFPEKr_2*g$L(9K(oARQA#@9SjIVOyGyykpT#}LFd(mxeJZ%S+oIvQ*g%5Qa zlrQVKlH?1ayy+u<;aR5Gd=9_g?GO8c?`nTLw|w#g zxqQL1v`5S`*F(0+o+D({eKL2y9kUa4<)Qm?LRRKjFhzsVA<4*)7hD1v|I===PTvKY zFPdxSIuH7;&XW*LA1QJZw<$FKtI|XkN^2oP=2pYvy392H(=wQ6sNnx&e3&b(kpvA$%bNXqBeZu9dBk?~daEvUwdzGA%eY`Ci8 zD9nFr@^dXSN;K}j_r)2jGheO8qv@F~d*gKS=c+2aB|k%U;-i($oxaVTb$dm( zUE&L!KJC1DFRNpc^o?_T`vKn+;rdsS(^@rem2aBgbb5t|e%ZEH3Q-UPove1fQi$l6 zZF{8<(J$NfN+F_Ow(XTdsH3d+_D#J~2zAB*+1svH3en7`X0KE)+^uq{8Mkl2MgA|z z{wdxr?YmkG#rfvGx9PqU+Sl(t>oH7rMa~MLYN<{+Mj!Ri5kohvWXC?IYKP7r=b>rC z33`t`PjdT_`3O|pE|iN=t>Z@5M~XbF{8w&W(36`tm~Hk-KLP63^}V6JaNd3U3Qcy= zr?ODtv))V`wChuc(kJ=`A zIx(5cQ2yTI-1Xh#om#D|-+zB~8%q%Kdg>sz!KGXAZ7RDwqx_LSqSvVF%0EI#uI9=| s)$DN%F=rjND?fF=zZoG3QAIV=oktwIJOBUy literal 0 HcmV?d00001 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..a7014c2 100644 --- a/src/controllers/reward.controller.ts +++ b/src/controllers/reward.controller.ts @@ -11,8 +11,22 @@ 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 => { @@ -39,9 +53,53 @@ 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 => { @@ -138,9 +196,30 @@ 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 => { diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 6c5e3b0..02094e4 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -1,62 +1,54 @@ -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 { - async getCurrentUser (req: Request, res: Response): Promise { - try { - const userId = (req as any).user.id - - 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, - 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 getting current user:', error) - res.status(500).json({ error: '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) - - 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, - }) - } catch (error) { - console.error('Error updating profile:', error) - res.status(500).json({ error: 'Internal server error' }) - } - } + /** + * @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 + */ + + /** + * @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 getUserById (req: Request, res: Response): Promise { try { @@ -75,6 +67,7 @@ export class UserController { firstName: user.firstName, lastName: user.lastName, avatar: user.avatar, + role: user.role, createdAt: user.createdAt, } @@ -113,37 +106,40 @@ export class UserController { } } - async updateWalletAddress (req: Request, res: Response): Promise { - try { - const userId = (req as any).user.id - const { walletAddress }: UpdateWalletData = req.body - - if (!this.isValidStellarAddress(walletAddress)) { - res.status(400).json({ error: 'Invalid Stellar wallet address' }) - - return - } - - const updatedUser = 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, - }) - } catch (error) { - console.error('Error updating wallet address:', 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 + */ private async findUserById (id: string): Promise { const mockUser: User = { @@ -156,6 +152,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 +172,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..5b68a3a --- /dev/null +++ b/swagger-server.ts @@ -0,0 +1,61 @@ +import express from 'express' +import swaggerUi from 'swagger-ui-express' +import swaggerJsdoc from 'swagger-jsdoc' +import cors from 'cors' +import * as fs from 'fs' +import * as path from 'path' + +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`) +}) From cab3b54cecf94b456b5e7f04af5eeec696387ca7 Mon Sep 17 00:00:00 2001 From: Emmzyemms Date: Tue, 10 Mar 2026 04:19:13 +0100 Subject: [PATCH 2/4] fix ci --- swagger-server.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/swagger-server.ts b/swagger-server.ts index 5b68a3a..b4aaa51 100644 --- a/swagger-server.ts +++ b/swagger-server.ts @@ -2,8 +2,6 @@ import express from 'express' import swaggerUi from 'swagger-ui-express' import swaggerJsdoc from 'swagger-jsdoc' import cors from 'cors' -import * as fs from 'fs' -import * as path from 'path' const app = express() const PORT = 5000 @@ -45,9 +43,9 @@ const options: swaggerJsdoc.Options = { ], }, apis: ['./src/controllers/**/*.ts', './src/docs/*.ts'], -}; +} -const specs = swaggerJsdoc(options); +const specs = swaggerJsdoc(options) app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs)) From 3314f53932ee1b9417fd79eacd6e1266eb0c621e Mon Sep 17 00:00:00 2001 From: Emmzyemms Date: Tue, 10 Mar 2026 05:13:11 +0100 Subject: [PATCH 3/4] fix test --- src/controllers/reward.controller.ts | 6 +- src/controllers/user.controller.ts | 90 +++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/controllers/reward.controller.ts b/src/controllers/reward.controller.ts index a7014c2..fdaaae4 100644 --- a/src/controllers/reward.controller.ts +++ b/src/controllers/reward.controller.ts @@ -30,7 +30,7 @@ export class RewardController { */ 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') @@ -103,7 +103,7 @@ export class RewardController { */ 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') @@ -223,7 +223,7 @@ export class RewardController { */ 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 02094e4..bf61e6c 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -23,6 +23,36 @@ export class UserController { * description: User not found */ + async getCurrentUser (req: Request, res: Response): Promise { + try { + 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, + 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) { + res.status(500).json({ error: 'Internal server error' }) + } + } + /** * @openapi * /users/profile: @@ -50,6 +80,33 @@ export class UserController { * description: Internal server error */ + async updateProfile (req: Request, res: Response): Promise { + try { + 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: 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) { + res.status(500).json({ error: 'Internal server error' }) + } + } + async getUserById (req: Request, res: Response): Promise { try { const { id } = req.params @@ -80,7 +137,7 @@ 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) @@ -141,6 +198,37 @@ export class UserController { * description: Internal server error */ + async updateWalletAddress (req: Request, res: Response): Promise { + try { + 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 user = await this.updateUserWallet(userId, walletAddress) + res.json({ + 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) { + res.status(500).json({ error: 'Internal server error' }) + } + } + private async findUserById (id: string): Promise { const mockUser: User = { id, From 228f3e279b1626aeab5b6cf9b23e5adb8214e47e Mon Sep 17 00:00:00 2001 From: Emmzyemms Date: Wed, 11 Mar 2026 09:22:41 +0100 Subject: [PATCH 4/4] fix --- src/controllers/user.controller.ts | 181 +++++++++++++++-------------- 1 file changed, 94 insertions(+), 87 deletions(-) diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index bf61e6c..892f625 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -23,36 +23,38 @@ export class UserController { * description: User not found */ - async getCurrentUser (req: Request, res: Response): Promise { - try { - 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, - 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) { - res.status(500).json({ error: 'Internal server error' }) - } - } + async getCurrentUser (req: Request, res: Response): Promise { + try { + 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, + 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 { + res.status(500).json({ error: 'Internal server error' }) + } + } + /** * @openapi * /users/profile: @@ -80,33 +82,34 @@ export class UserController { * description: Internal server error */ - async updateProfile (req: Request, res: Response): Promise { - try { - 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: 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) { - res.status(500).json({ error: 'Internal server error' }) - } - } + async updateProfile (req: Request, res: Response): Promise { + try { + 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: 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 { + res.status(500).json({ error: 'Internal server error' }) + } + } + async getUserById (req: Request, res: Response): Promise { try { const { id } = req.params @@ -137,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 } @@ -151,13 +155,14 @@ 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' }) } @@ -198,37 +203,39 @@ export class UserController { * description: Internal server error */ - async updateWalletAddress (req: Request, res: Response): Promise { - try { - 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 user = await this.updateUserWallet(userId, walletAddress) - res.json({ - 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) { - res.status(500).json({ error: 'Internal server error' }) - } - } + async updateWalletAddress (req: Request, res: Response): Promise { + try { + 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 user = await this.updateUserWallet(userId, walletAddress) + res.json({ + 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 { + res.status(500).json({ error: 'Internal server error' }) + } + } + private async findUserById (id: string): Promise { const mockUser: User = { id,