diff --git a/README.md b/README.md new file mode 100644 index 0000000..5677031 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# SecureCoder diff --git a/apps/backend/package.json b/apps/backend/package.json new file mode 100644 index 0000000..5d519b9 --- /dev/null +++ b/apps/backend/package.json @@ -0,0 +1,23 @@ +{ + "name": "secure-coder-backend", + "private": true, + "dependencies": { + "@prisma/client": "^6.9.0", + "axios": "^1.9.0", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^4.21.2" + }, + "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977", + "devDependencies": { + "@types/cors": "^2.8.18", + "@types/express": "^4.17.22", + "@types/node": "^22.15.24", + "prisma": "^6.8.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "scripts": { + "dev": "ts-node src/index.ts" + } +} \ No newline at end of file diff --git a/apps/backend/pnpm-lock.yaml b/apps/backend/pnpm-lock.yaml new file mode 100644 index 0000000..fc72031 --- /dev/null +++ b/apps/backend/pnpm-lock.yaml @@ -0,0 +1,1020 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@prisma/client': + specifier: ^6.8.2 + version: 6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3) + axios: + specifier: ^1.9.0 + version: 1.9.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + express: + specifier: ^4.21.2 + version: 4.21.2 + devDependencies: + '@types/cors': + specifier: ^2.8.18 + version: 2.8.18 + '@types/express': + specifier: ^4.17.22 + version: 4.17.22 + '@types/node': + specifier: ^22.15.24 + version: 22.15.24 + prisma: + specifier: ^6.8.2 + version: 6.8.2(typescript@5.8.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.15.24)(typescript@5.8.3) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + +packages: + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@prisma/client@6.8.2': + resolution: {integrity: sha512-5II+vbyzv4si6Yunwgkj0qT/iY0zyspttoDrL3R4BYgLdp42/d2C8xdi9vqkrYtKt9H32oFIukvyw3Koz5JoDg==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.8.2': + resolution: {integrity: sha512-ZJY1fF4qRBPdLQ/60wxNtX+eu89c3AkYEcP7L3jkp0IPXCNphCYxikTg55kPJLDOG6P0X+QG5tCv6CmsBRZWFQ==} + + '@prisma/debug@6.8.2': + resolution: {integrity: sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg==} + + '@prisma/engines-version@6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e': + resolution: {integrity: sha512-Rkik9lMyHpFNGaLpPF3H5q5TQTkm/aE7DsGM5m92FZTvWQsvmi6Va8On3pWvqLHOt5aPUvFb/FeZTmphI4CPiQ==} + + '@prisma/engines@6.8.2': + resolution: {integrity: sha512-XqAJ//LXjqYRQ1RRabs79KOY4+v6gZOGzbcwDQl0D6n9WBKjV7qdrbd042CwSK0v0lM9MSHsbcFnU2Yn7z8Zlw==} + + '@prisma/fetch-engine@6.8.2': + resolution: {integrity: sha512-lCvikWOgaLOfqXGacEKSNeenvj0n3qR5QvZUOmPE2e1Eh8cMYSobxonCg9rqM6FSdTfbpqp9xwhSAOYfNqSW0g==} + + '@prisma/get-platform@6.8.2': + resolution: {integrity: sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow==} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/body-parser@1.19.5': + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.18': + resolution: {integrity: sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==} + + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + + '@types/express@4.17.22': + resolution: {integrity: sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==} + + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node@22.15.24': + resolution: {integrity: sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.9.0: + resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + prisma@6.8.2: + resolution: {integrity: sha512-JNricTXQxzDtRS7lCGGOB4g5DJ91eg3nozdubXze3LpcMl1oWwcFddrj++Up3jnRE6X/3gB/xz3V+ecBk/eEGA==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + +snapshots: + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@prisma/client@6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3)': + optionalDependencies: + prisma: 6.8.2(typescript@5.8.3) + typescript: 5.8.3 + + '@prisma/config@6.8.2': + dependencies: + jiti: 2.4.2 + + '@prisma/debug@6.8.2': {} + + '@prisma/engines-version@6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e': {} + + '@prisma/engines@6.8.2': + dependencies: + '@prisma/debug': 6.8.2 + '@prisma/engines-version': 6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e + '@prisma/fetch-engine': 6.8.2 + '@prisma/get-platform': 6.8.2 + + '@prisma/fetch-engine@6.8.2': + dependencies: + '@prisma/debug': 6.8.2 + '@prisma/engines-version': 6.8.0-43.2060c79ba17c6bb9f5823312b6f6b7f4a845738e + '@prisma/get-platform': 6.8.2 + + '@prisma/get-platform@6.8.2': + dependencies: + '@prisma/debug': 6.8.2 + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.15.24 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.15.24 + + '@types/cors@2.8.18': + dependencies: + '@types/node': 22.15.24 + + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 22.15.24 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express@4.17.22': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.7 + + '@types/http-errors@2.0.4': {} + + '@types/mime@1.3.5': {} + + '@types/node@22.15.24': + dependencies: + undici-types: 6.21.0 + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.15.24 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 22.15.24 + '@types/send': 0.17.4 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + arg@4.1.3: {} + + array-flatten@1.1.1: {} + + asynckit@0.4.0: {} + + axios@1.9.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + create-require@1.1.1: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + destroy@1.2.0: {} + + diff@4.0.2: {} + + dotenv@16.5.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + follow-redirects@1.15.9: {} + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + jiti@2.4.2: {} + + make-error@1.3.6: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + negotiator@0.6.3: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + parseurl@1.3.3: {} + + path-to-regexp@0.1.12: {} + + prisma@6.8.2(typescript@5.8.3): + dependencies: + '@prisma/config': 6.8.2 + '@prisma/engines': 6.8.2 + optionalDependencies: + typescript: 5.8.3 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.1: {} + + toidentifier@1.0.1: {} + + ts-node@10.9.2(@types/node@22.15.24)(typescript@5.8.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.15.24 + acorn: 8.14.1 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typescript@5.8.3: {} + + undici-types@6.21.0: {} + + unpipe@1.0.0: {} + + utils-merge@1.0.1: {} + + v8-compile-cache-lib@3.0.1: {} + + vary@1.1.2: {} + + yn@3.1.1: {} diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts new file mode 100644 index 0000000..6d28252 --- /dev/null +++ b/apps/backend/src/index.ts @@ -0,0 +1,14 @@ +import express from 'express'; +import cors from 'cors'; +import exercisesRouter from './routes/exercises'; // adjust path as needed + +const app = express(); +app.use(cors()); +app.use(express.json()); + +app.use('/api/exercises', exercisesRouter); + +const PORT = process.env.PORT || 4000; +app.listen(PORT, () => { + console.log(`Server listening on port ${PORT}`); +}); diff --git a/apps/backend/src/prisma/client.ts b/apps/backend/src/prisma/client.ts new file mode 100644 index 0000000..e9ae4c4 --- /dev/null +++ b/apps/backend/src/prisma/client.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from '@prisma/client'; +const prisma = new PrismaClient(); +export default prisma; diff --git a/apps/backend/src/routes/exercises.ts b/apps/backend/src/routes/exercises.ts new file mode 100644 index 0000000..f18283a --- /dev/null +++ b/apps/backend/src/routes/exercises.ts @@ -0,0 +1,89 @@ +import { Router, Request, Response } from 'express'; +import axios from "axios"; + +import prisma from '../prisma/client'; +import { buildSubmission, submitToJudge0 } from '../services/judge0client'; + +const router = Router(); + +router.get('/', async (req: Request, res: Response) => { + const exercises = await prisma.exercise.findMany(); + res.json(exercises); +}); + +router.post('/', async (req, res) => { + const newexercise = await prisma.exercise.create({ + data: req.body + }); + res.status(201).json(newexercise); +}); + +router.get('/:id', async (req: Request, res: Response) => { + const { id } = req.params; + const exercise = await prisma.exercise.findUnique({ where: { id } }); + if (!exercise) return res.status(404).json({ error: 'Not found' }); + res.json(exercise); +}); + +router.put('/:id', async (req: Request, res: Response) => { + const { id } = req.params; + try { + const updated = await prisma.exercise.update({ + where: { id }, + data: req.body, + }); + res.json(updated); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Failed to update exercise' }); + } +}); + +router.delete('/:id', async (req: Request, res: Response) => { + const { id } = req.params; + + try { + await prisma.exercise.delete({ where: { id } }); + res.status(204).send(); // No content on successful delete + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Failed to delete exercise' }); + } +}); + +router.post('/:id/submissions', async (req: Request, res: Response) => { + const { id } = req.params; + let { input } = req.body; + + const exercise = await prisma.exercise.findUnique({ where: { id } }); + if (!exercise) return res.status(404).json({ error: 'Not found' }); + + const source = exercise.type === 'offensive' ? exercise.vulnerableCode : input; + input = exercise.type === 'offensive' ? input : null; + + try { + const submission = await buildSubmission(source, exercise.driverCode, input); + const result = await submitToJudge0(submission); + res.json(result); + } catch (error) { + console.error('Error submitting or fetching code:', error); + res.status(500).json({ stderr: 'Failed to execute code' }); + } +}); + +router.post('/validate', async (req: Request, res: Response) => { + const { type, driver, vulnerableCode, solution } = req.body; + const source = type === 'offensive' ? vulnerableCode : solution; + const input = type === 'offensive' ? solution : null; + + try { + const submission = await buildSubmission(source, driver, input); + const result = await submitToJudge0(submission); + res.json(result); + } catch (error) { + console.error('Error submitting or fetching code:', error); + res.status(500).json({ stderr: 'Failed to execute code' }); + } +}); + +export default router; \ No newline at end of file diff --git a/apps/backend/src/services/judge0client.ts b/apps/backend/src/services/judge0client.ts new file mode 100644 index 0000000..6151eeb --- /dev/null +++ b/apps/backend/src/services/judge0client.ts @@ -0,0 +1,81 @@ +import axios from "axios"; + +enum Judge0Status { + InQueue = 1, + Processing, + Accepted, + WrongAnswer, + TimeLimitExceeded, + CompilationError, + RuntimeErrorSIGSEGV, + RuntimeErrorSIGXFSZ, + RuntimeErrorSIGFPE, + RuntimeErrorSIGABRT, + RuntimeErrorNZEC, + RuntimeErrorOther, + InternalError, + ExecFormatError +} + +export interface Judge0ApiRequest { + source_code: string, + language_id: number, + stdin: string, + command_line_arguments: string, + compiler_options: string +} + +export interface Judge0ApiResponse { + status: number, + feedback?: string, + compile_output?: string, +} + +const api = axios.create({ + baseURL: process.env.JUDGE0_URL || 'http://localhost:2358', +}); + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export async function buildSubmission(source: string, driver: string, input: string | null): Promise { + return { + source_code: btoa(source + '\n' + driver), + language_id: 50, + stdin: input ? btoa(input) : '', + command_line_arguments: input ?? '', + compiler_options: '-fstack-protector-all', + }; +} + +export async function submitToJudge0(payload: Judge0ApiRequest): Promise { + const submissionResponse = await api.post('/submissions?base64_encoded=true', payload); + const submissionToken = submissionResponse.data.token; + + let result; + let status: number | undefined; + + while (true) { + result = await api.get(`/submissions/${submissionToken}?base64_encoded=true`); + status = result.data.status?.id; + if (status === Judge0Status.InQueue || status === Judge0Status.Processing) { + await sleep(1000); + } else { + break; + } + } + + const response: Judge0ApiResponse = { + status: status ?? Judge0Status.InternalError, + compile_output: result.data.compile_output ? atob(result.data.compile_output) : '', + }; + + if (status === Judge0Status.CompilationError) { + response.feedback = 'Compilation error: ' + response.compile_output; + } else if (result.data.stderr) { + response.feedback = atob(result.data.stderr); + } + + return response; +} diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json new file mode 100644 index 0000000..b080c7e --- /dev/null +++ b/apps/backend/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "rootDir": "src", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "paths": { + "@prisma/client": [ + "../../node_modules/@prisma/client" + ] + } + } +} \ No newline at end of file diff --git a/apps/frontend/eslint.config.js b/apps/frontend/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/apps/frontend/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/apps/frontend/index.html b/apps/frontend/index.html new file mode 100644 index 0000000..5e6c799 --- /dev/null +++ b/apps/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + + SecureCoder: Secure Programming Exercises + + + +
+ + + + \ No newline at end of file diff --git a/apps/frontend/package.json b/apps/frontend/package.json new file mode 100644 index 0000000..3e3fb54 --- /dev/null +++ b/apps/frontend/package.json @@ -0,0 +1,44 @@ +{ + "name": "secure-coder-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@tailwindcss/postcss": "^4.1.8", + "@tailwindcss/vite": "^4.1.8", + "axios": "^1.9.0", + "framer-motion": "^12.15.0", + "monaco-editor": "^0.52.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-hook-form": "^7.56.4", + "react-icons": "^5.5.0", + "react-markdown": "^10.1.0", + "react-monaco-editor": "^0.58.0", + "react-router-dom": "^6.30.1" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.21", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "postcss": "^8.5.4", + "tailwindcss": "^3.4.17", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5" + }, + "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977" +} \ No newline at end of file diff --git a/apps/frontend/postcss.config.js b/apps/frontend/postcss.config.js new file mode 100644 index 0000000..49c0612 --- /dev/null +++ b/apps/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/frontend/public/vite.svg b/apps/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/src/App.css b/apps/frontend/src/App.css new file mode 100644 index 0000000..730fed7 --- /dev/null +++ b/apps/frontend/src/App.css @@ -0,0 +1,89 @@ +/* #root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} */ + +#root { + width: 100%; + margin: 0 auto; + padding: 1rem 2rem; + /* less padding horizontally if you want */ + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} + +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +/* In App.css */ + +.wide-layout { + max-width: none !important; + text-align: left !important; + padding-left: 4rem !important; + padding-right: 4rem !important; +} + +.markdown-body { + text-align: left; +} + +.markdown-body { + margin: 0; + /* Remove default margin */ + text-align: left; + display: block; +} + +.markdown-body pre { + background-color: #f5f5f5; + padding: 1rem; + border-radius: 5px; + border: 1px solid #ccc; + overflow-x: auto; +} + +.markdown-body code { + font-family: 'Courier New', Courier, monospace; +} + +.markdown-body p+p { + margin-top: 1em; +} \ No newline at end of file diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx new file mode 100644 index 0000000..a9e871b --- /dev/null +++ b/apps/frontend/src/App.tsx @@ -0,0 +1,56 @@ +import './App.css' +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { useParams } from "react-router-dom"; + +import Layout from './components/Layout'; +import Exercise from "./components/Exercise"; +import ExerciseList from "./components/ExerciseList"; +import ExerciseForm from "./components/ExerciseForm"; +import LoginPage from './pages/LoginPage'; +import { BreadcrumbProvider } from './context/BreadcrumbsContext'; +import { AuthProvider } from './auth/AuthContext'; +import { PrivateRoute } from './auth/PrivateRoute'; + +export default function App() { + return ( + + + + + }> + } /> + } /> + + + + } /> + + + + } /> + } /> + } /> + + + + + + ); +} + +// Wrapper to extract id param and pass to ExerciseForm + +function ExerciseWithId() { + const { id } = useParams(); + if (!id) return
Invalid exercise ID
; + return ; +} + +function ExerciseFormWithId() { + const { id } = useParams(); + if (!id) return
Invalid exercise ID
; + return ; +} + diff --git a/apps/frontend/src/assets/react.svg b/apps/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/src/auth/AuthContext.tsx b/apps/frontend/src/auth/AuthContext.tsx new file mode 100644 index 0000000..8160182 --- /dev/null +++ b/apps/frontend/src/auth/AuthContext.tsx @@ -0,0 +1,66 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; + +interface AuthContextType { + isAuthenticated: boolean; + isSuperUser: boolean; + login: (password: string) => boolean; + logout: () => void; + isLoading: boolean; +} + +const AuthContext = createContext(undefined); + +const ADMIN_PASSWORD = 'password'; // Move to .env for better security +const SUPER_PASSWORD = 'password123'; // Move to .env for better security + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [isSuperUser, setIsSuperUser] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const stored = localStorage.getItem('isAuthenticated'); + if (stored === 'true') { + setIsAuthenticated(true); + } + const storedSuper = localStorage.getItem('isSuperUser'); + if (storedSuper === 'true') { + setIsSuperUser(true); + } + setIsLoading(false); + }, []); + + const login = (password: string) => { + if (password === ADMIN_PASSWORD) { + setIsAuthenticated(true); + localStorage.setItem('isAuthenticated', 'true'); + return true; + } else if (password == SUPER_PASSWORD) { + setIsAuthenticated(true); + setIsSuperUser(true); + localStorage.setItem('isAuthenticated', 'true'); + localStorage.setItem('isSuperUser', 'true'); + return true; + } + return false; + }; + + const logout = () => { + setIsAuthenticated(false); + setIsSuperUser(false); + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('isSuperUser'); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error('useAuth must be used within AuthProvider'); + return ctx; +}; diff --git a/apps/frontend/src/auth/PrivateRoute.tsx b/apps/frontend/src/auth/PrivateRoute.tsx new file mode 100644 index 0000000..436f1c5 --- /dev/null +++ b/apps/frontend/src/auth/PrivateRoute.tsx @@ -0,0 +1,12 @@ +import { Navigate } from 'react-router-dom'; +import { useAuth } from './AuthContext'; + +export const PrivateRoute = ({ children }: { children: JSX.Element }) => { + const { isAuthenticated, isLoading } = useAuth(); + + if (isLoading) { + return
Loading...
; // Or a spinner + } + + return isAuthenticated ? children : ; +}; \ No newline at end of file diff --git a/apps/frontend/src/components/Breadcrumbs.tsx b/apps/frontend/src/components/Breadcrumbs.tsx new file mode 100644 index 0000000..73e988d --- /dev/null +++ b/apps/frontend/src/components/Breadcrumbs.tsx @@ -0,0 +1,19 @@ +import { Link } from 'react-router-dom'; +import { useBreadcrumb } from '../context/BreadcrumbsContext'; + +const Breadcrumbs = () => { + const { breadcrumb } = useBreadcrumb(); + + return ( + + ); +}; + +export default Breadcrumbs; diff --git a/apps/frontend/src/components/Exercise.tsx b/apps/frontend/src/components/Exercise.tsx new file mode 100644 index 0000000..9ce97f6 --- /dev/null +++ b/apps/frontend/src/components/Exercise.tsx @@ -0,0 +1,221 @@ +import { useForm } from "react-hook-form"; +import { useLocation } from "react-router-dom"; +import Editor from "@monaco-editor/react"; +import { useState, useEffect } from "react"; +import axios from "axios"; +import ReactMarkdown from 'react-markdown'; + +import { useBreadcrumb } from "../context/BreadcrumbsContext"; +// import { HintsAccordion } from "./Hints"; + +type ExerciseFormData = { + exerciseId: number; + code: string; + input: string; +}; + +type ExerciseFormProps = { + exerciseId?: string; +}; + +function Alert({ type = 'info', children }) { + // Define styles per alert type + const baseClasses = "flex items-center p-4 rounded border text-left"; + const typeStyles = { + info: "bg-blue-100 border-blue-400 text-blue-700", + success: "bg-green-100 border-green-400 text-green-700", + warning: "bg-yellow-100 border-yellow-400 text-yellow-700", + danger: "bg-red-100 border-red-400 text-red-700", + }; + + return ( +
+
{children}
+
+ ); +} + +interface ReadOnlyCodeEditorProps { + code: string; +} + +function ReadOnlyCodeEditor({ code }: ReadOnlyCodeEditorProps) { + return ( + + ); +} + +interface EditableCodeEditorProps { + code: string; + setCode: (val: string) => void; +} + +function EditableCodeEditor({ code, setCode }: EditableCodeEditorProps) { + return ( + setCode(val || "")} + options={{ + minimap: { enabled: false }, + readOnly: false, + scrollBeyondLastLine: false, + theme: "vs-dark", + }} + /> + ); +} + +export default function Exercise({ exerciseId }: ExerciseFormProps) { + const location = useLocation(); + const { setBreadcrumb } = useBreadcrumb(); + const { register, handleSubmit, reset } = useForm(); + + const [vulnerableCode, setVulnerableCode] = useState("// vulnerable code"); + const [description, setDescription] = useState(""); + const [title, setTitle] = useState(""); + const [input, setInput] = useState(""); + const [explanation, setExplanation] = useState(""); + const [results, setResults] = useState(null); + const [tag, setTag] = useState(""); + const [type, setType] = useState(""); + // const [hints, setHints] = useState([]); + + useEffect(() => { + if (exerciseId) { + axios + .get(`http://localhost:4000/api/exercises/${exerciseId}`) + .then((res) => { + const ex = res.data; + setVulnerableCode(ex.vulnerableCode); + setDescription(ex.description); + setTitle(ex.title); + setExplanation(ex.explanation); + setInput(ex.input); + setTag(ex.tags[0]); + setType(ex.type); + // setHints(ex.hints); + }) + .catch((err) => { + console.error("Failed to load exercise:", err); + alert("Failed to load exercise for editing."); + }); + } + setBreadcrumb([ + { label: 'Home', path: '/' }, + { label: tag }, + { label: title }, + ]); + }, [exerciseId, location.state, reset, setBreadcrumb, tag, title]); + + const onSubmit = async (data: ExerciseFormData) => { + let finalInput = data.input; + + if (type === "defensive") { + finalInput = vulnerableCode; // use code as input + } + + if (!finalInput) { + alert("Must provide input"); + return; + } + + const payload = { + ...data, + input: finalInput, + }; + + try { + if (exerciseId) { + const res = await axios.post( + `http://localhost:4000/api/exercises/${exerciseId}/submissions`, + payload + ); + setResults(res.data); + alert("Exercise submitted!"); + } + } catch (error) { + alert("Error saving exercise."); + console.error(error); + } + }; + + return ( + <> +
+
+

{title}

+
+ {description} +
+
+ + {/* Code Editors */} + {type === 'defensive' ? ( + + ) : ( + + )} + + {type === 'offensive' && ( +
+ + +
+ )} + +
+ {results && ( + <> +
+ {results.status == 3 + ? Success! {results.feedback} + : Try again. {results.feedback}} +
+ {results.status == 3 && ( + <> +

+

Explanation

+
+ {explanation} +
+ + )} + + )} +
+ + {/*
+ +
*/} +
+ +
+ + + ); +} diff --git a/apps/frontend/src/components/ExerciseForm.tsx b/apps/frontend/src/components/ExerciseForm.tsx new file mode 100644 index 0000000..40b3df3 --- /dev/null +++ b/apps/frontend/src/components/ExerciseForm.tsx @@ -0,0 +1,417 @@ +import { useForm } from "react-hook-form"; +import { useNavigate, useLocation } from "react-router-dom"; +import Editor from "@monaco-editor/react"; +import { useState, useEffect } from "react"; +import axios from "axios"; + +import { useBreadcrumb } from "../context/BreadcrumbsContext"; + +type ExerciseFormData = { + type: string; + title: string; + description: string; + driverCode: string; + vulnerableCode: string; + input: string; + solution: string; + hints: string; + explanation: string; + tags: string; +}; + +type ExerciseFormProps = { + exerciseId?: string; +}; + +function Alert({ type = 'info', children }) { + // Define styles per alert type + const baseClasses = "flex items-center p-4 rounded border text-left"; + const typeStyles = { + info: "bg-blue-100 border-blue-400 text-blue-700", + success: "bg-green-100 border-green-400 text-green-700", + warning: "bg-yellow-100 border-yellow-400 text-yellow-700", + danger: "bg-red-100 border-red-400 text-red-700", + }; + + return ( +
+
{children}
+
+ ); +} + +const codeDescriptions: Record = { + 1: "In Queue", + 2: "Processing", + 3: "Success (zero exit code)", + 4: "Wrong Answer", + 5: "Time Limit Exceeded", + 6: "Compilation Error", + 7: "Runtime Error (SIGSEGV)", + 8: "Runtime Error (SIGXFSZ)", + 9: "Runtime Error (SIGFPE)", + 10: "Runtime Error (SIGABRT)", + 11: "Failure (non-zero exit code)", + 12: "Runtime Error (Other)", + 13: "Internal Error", + 14: "Exec Format Error" +}; + +const getDescription = (code: number): string => { + return codeDescriptions[code] || "Unknown result code"; +}; + +export default function ExerciseForm({ exerciseId }: ExerciseFormProps) { + const navigate = useNavigate(); + const location = useLocation(); + const { setBreadcrumb } = useBreadcrumb(); + const { register, handleSubmit, reset, getValues } = useForm(); + + const [driverCode, setDriverCode] = useState("// starter/driver code"); + const [vulnerableCode, setVulnerableCode] = useState("// vulnerable code"); + const [solution, setSolution] = useState("// solution code"); + const [validateResults, setValidateResults] = useState(null); + + // Store current exercise data for cloning + const [currentExerciseData, setCurrentExerciseData] = useState(null); + + useEffect(() => { + if (exerciseId) { + axios + .get(`http://localhost:4000/api/exercises/${exerciseId}`) + .then((res) => { + const ex = res.data; + reset({ + type: ex.type, + title: ex.title, + description: ex.description, + input: ex.input, + hints: ex.hints.join(", "), + explanation: ex.explanation, + tags: ex.tags.join(", "), + }); + setDriverCode(ex.driverCode); + setVulnerableCode(ex.vulnerableCode); + setSolution(ex.solution); + + // Save current exercise data for clone button + setCurrentExerciseData({ + type: ex.type, + title: ex.title, + description: ex.description, + input: ex.input, + hints: ex.hints.join(", "), + explanation: ex.explanation, + tags: ex.tags.join(", "), + driverCode: ex.driverCode, + vulnerableCode: ex.vulnerableCode, + solution: ex.solution, + }); + setBreadcrumb([ + { label: 'Home', path: '/' }, + { label: 'Edit' }, + { label: ex.title } + ]) + }) + .catch((err) => { + console.error("Failed to load exercise:", err); + alert("Failed to load exercise for editing."); + }); + } else { + // create mode: check for clone data from location.state + const clonedExercise = location.state?.clonedExercise; + if (clonedExercise) { + reset({ + type: clonedExercise.type, + title: clonedExercise.title, + description: clonedExercise.description || "", + input: clonedExercise.input || "", + hints: Array.isArray(clonedExercise.hints) + ? clonedExercise.hints.join(", ") + : clonedExercise.hints || "", + explanation: clonedExercise.explanation || "", + tags: Array.isArray(clonedExercise.tags) + ? clonedExercise.tags.join(", ") + : clonedExercise.tags || "", + }); + setDriverCode(clonedExercise.driverCode || "// starter/driver code"); + setVulnerableCode(clonedExercise.vulnerableCode || "// vulnerable code"); + setSolution(clonedExercise.solution || "// triggering input or patched function"); + } + setBreadcrumb([ + { label: 'Home', path: '/' }, + { label: 'New exercise' } + ]) + } + }, [exerciseId, location.state, reset, setBreadcrumb]); + + const onSubmit = async (data: ExerciseFormData) => { + const payload = { + ...data, + driverCode, + vulnerableCode, + solution, + hints: data.hints + ? data.hints.split(",").map((hint) => hint.trim()).filter(Boolean) + : [], + tags: data.tags + .split(",") + .map((tag) => tag.trim()) + .filter(Boolean), + }; + + try { + if (exerciseId) { + await axios.put( + `http://localhost:4000/api/exercises/${exerciseId}`, + payload + ); + alert("Exercise updated!"); + } else { + const response = await axios.post("http://localhost:4000/api/exercises", payload); + alert("Exercise created!"); + navigate(`/exercises/${response.data.id}/edit`); + } + } catch (error) { + alert("Error saving exercise."); + console.error(error); + } + }; + + const handleValidateClick = async () => { + const formValues = getValues(); // Get values from the form + const payload = { + type: formValues.type, + driver: driverCode, + vulnerableCode: vulnerableCode, + solution: solution + }; + + if (!vulnerableCode) { + alert("Must provide vulnerable function"); + return; + } + if (!driverCode) { + alert("Must provide driver function"); + return; + } + if (!solution) { + alert("Must provide solution"); + return; + } + + try { + const res = await axios.post( + `http://localhost:4000/api/exercises/validate`, + payload + ); + setValidateResults(res.data); + console.log(res.data); + alert("Exercise submitted for validation..."); + } catch (error) { + alert("Error saving exercise."); + console.error(error); + } + + console.log("Validation Payload:", payload); + + }; + + // Handler for Clone button on edit page + const handleCloneClick = () => { + if (!currentExerciseData) return; + // Remove any identifying id info (if present) + const cloneData = { ...currentExerciseData }; + navigate("/exercises/new", { state: { clonedExercise: cloneData } }); + }; + + return ( +
+

+ {exerciseId ? "Edit Exercise" : "Create Exercise"} +

+ +
+
+ + + + +
+ +
+ +
Provide a prompt that describes the task to complete (Markdown supported):
+