diff --git a/Justfile b/Justfile index a9024ab..fe4ea0f 100644 --- a/Justfile +++ b/Justfile @@ -11,6 +11,10 @@ set dotenv-load @user $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true": fastify start src/api/user/default.js +# For launching the images api +@images $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true": + fastify start src/api/images/default.js + @scoreStore $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true": fastify start src/api/scoreStore/default.js @@ -55,4 +59,4 @@ set dotenv-load forge verify-contract --chain-id 43113 --rpc-url=${AVAX_RPC_URL} --watch ${AVAX_CONTRACT_ADDR} @status: - docker compose -f docker/docker-compose.yml ps \ No newline at end of file + docker compose -f docker/docker-compose.yml ps diff --git a/package.json b/package.json index 7eedbe9..32cd1ca 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "@fastify/cookie": "^11.0.2", "@fastify/env": "^5.0.2", "@fastify/jwt": "^9.1.0", + "@fastify/multipart": "^9.2.1", "axios": "^1.10.0", "base32.js": "^0.1.0", "bcrypt": "^6.0.0", @@ -13,6 +14,7 @@ "fastify-cli": "^7.4.0", "pino": "^9.7.0", "prom-client": "^15.1.3", + "sharp": "^0.34.4", "solhint": "^6.0.0" }, "type": "module", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91f44c7..c40ec77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@fastify/jwt': specifier: ^9.1.0 version: 9.1.0 + '@fastify/multipart': + specifier: ^9.2.1 + version: 9.2.1 axios: specifier: ^1.10.0 version: 1.10.0 @@ -47,6 +50,9 @@ importers: prom-client: specifier: ^15.1.3 version: 15.1.3 + sharp: + specifier: ^0.34.4 + version: 0.34.4 solhint: specifier: ^6.0.0 version: 6.0.0(typescript@5.8.3) @@ -88,6 +94,9 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@esbuild/aix-ppc64@0.25.6': resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} engines: {node: '>=18'} @@ -252,12 +261,18 @@ packages: '@fastify/ajv-compiler@4.0.2': resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==} + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + '@fastify/cookie@11.0.2': resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} '@fastify/deepmerge@2.0.2': resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==} + '@fastify/deepmerge@3.1.0': + resolution: {integrity: sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==} + '@fastify/env@5.0.2': resolution: {integrity: sha512-4m/jHS3s/G/DBJVODob9sxGUei/Ij8JFbA2PYqBfoihTm+Qqae2xD9xhez68UFZu1d4SNJPIb6uAOwbNvRYw+A==} @@ -276,6 +291,9 @@ packages: '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + '@fastify/multipart@9.2.1': + resolution: {integrity: sha512-U4221XDMfzCUtfzsyV1/PkR4MNgKI0158vUUyn/oF2Tl6RxMc+N7XYLr5fZXQiEC+Fmw5zFaTjxsTGTgtDtK+g==} + '@fastify/proxy-addr@5.0.0': resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==} @@ -283,6 +301,132 @@ packages: resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} engines: {node: '>=10.10.0'} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.4': + resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.4': + resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.3': + resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.3': + resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.3': + resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.3': + resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.3': + resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.3': + resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.3': + resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.4': + resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.4': + resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.4': + resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.4': + resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.4': + resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.4': + resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.4': + resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.4': + resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.4': + resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.4': + resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.4': + resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -753,6 +897,10 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dotenv-expand@10.0.0: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} @@ -1452,6 +1600,10 @@ packages: set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + sharp@0.34.4: + resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -1667,6 +1819,11 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.7.0 + optional: true + '@esbuild/aix-ppc64@0.25.6': optional: true @@ -1753,6 +1910,8 @@ snapshots: ajv-formats: 3.0.1(ajv@8.17.1) fast-uri: 3.0.6 + '@fastify/busboy@3.2.0': {} + '@fastify/cookie@11.0.2': dependencies: cookie: 1.0.2 @@ -1760,6 +1919,8 @@ snapshots: '@fastify/deepmerge@2.0.2': {} + '@fastify/deepmerge@3.1.0': {} + '@fastify/env@5.0.2': dependencies: env-schema: 6.0.1 @@ -1785,6 +1946,14 @@ snapshots: dependencies: dequal: 2.0.3 + '@fastify/multipart@9.2.1': + dependencies: + '@fastify/busboy': 3.2.0 + '@fastify/deepmerge': 3.1.0 + '@fastify/error': 4.2.0 + fastify-plugin: 5.0.1 + secure-json-parse: 4.0.0 + '@fastify/proxy-addr@5.0.0': dependencies: '@fastify/forwarded': 3.0.0 @@ -1792,6 +1961,94 @@ snapshots: '@humanwhocodes/momoa@2.0.4': {} + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.3 + optional: true + + '@img/sharp-darwin-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.3': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.3': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + optional: true + + '@img/sharp-linux-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.3 + optional: true + + '@img/sharp-linux-arm@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.3 + optional: true + + '@img/sharp-linux-ppc64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.3 + optional: true + + '@img/sharp-linux-s390x@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.3 + optional: true + + '@img/sharp-linux-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.3 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + optional: true + + '@img/sharp-wasm32@0.34.4': + dependencies: + '@emnapi/runtime': 1.5.0 + optional: true + + '@img/sharp-win32-arm64@0.34.4': + optional: true + + '@img/sharp-win32-ia32@0.34.4': + optional: true + + '@img/sharp-win32-x64@0.34.4': + optional: true + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -2183,6 +2440,8 @@ snapshots: detect-libc@2.0.4: {} + detect-libc@2.1.2: {} + dotenv-expand@10.0.0: {} dotenv@16.6.1: {} @@ -2914,6 +3173,35 @@ snapshots: set-cookie-parser@2.7.1: {} + sharp@0.34.4: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.4 + '@img/sharp-darwin-x64': 0.34.4 + '@img/sharp-libvips-darwin-arm64': 1.2.3 + '@img/sharp-libvips-darwin-x64': 1.2.3 + '@img/sharp-libvips-linux-arm': 1.2.3 + '@img/sharp-libvips-linux-arm64': 1.2.3 + '@img/sharp-libvips-linux-ppc64': 1.2.3 + '@img/sharp-libvips-linux-s390x': 1.2.3 + '@img/sharp-libvips-linux-x64': 1.2.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + '@img/sharp-linux-arm': 0.34.4 + '@img/sharp-linux-arm64': 0.34.4 + '@img/sharp-linux-ppc64': 0.34.4 + '@img/sharp-linux-s390x': 0.34.4 + '@img/sharp-linux-x64': 0.34.4 + '@img/sharp-linuxmusl-arm64': 0.34.4 + '@img/sharp-linuxmusl-x64': 0.34.4 + '@img/sharp-wasm32': 0.34.4 + '@img/sharp-win32-arm64': 0.34.4 + '@img/sharp-win32-ia32': 0.34.4 + '@img/sharp-win32-x64': 0.34.4 + simple-concat@1.0.1: {} simple-get@4.0.1: diff --git a/src/api/auth/remove.js b/src/api/auth/remove.js index 90579af..3f653c6 100644 --- a/src/api/auth/remove.js +++ b/src/api/auth/remove.js @@ -1,5 +1,5 @@ -import authDB from '../../utils/authDB'; -import { authUserRemove } from '../../utils/authUserRemove'; +import authDB from '../../utils/authDB.js'; +import { authUserRemove } from '../../utils/authUserRemove.js'; /** * @param {import('fastify').FastifyRequest} request diff --git a/src/api/images/dImage.js b/src/api/images/dImage.js new file mode 100644 index 0000000..5b18db7 --- /dev/null +++ b/src/api/images/dImage.js @@ -0,0 +1,10 @@ +export async function dImage(request, reply, fastify, deleteImage) { + try { + const imageId = Number(request.params.imageId); + deleteImage.run(imageId); + return reply.code(200).send({ msg: "Image deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/images/default.js b/src/api/images/default.js new file mode 100644 index 0000000..569be87 --- /dev/null +++ b/src/api/images/default.js @@ -0,0 +1,86 @@ +import fastifyJWT from '@fastify/jwt'; +import fastifyCookie from '@fastify/cookie'; +import Database from 'better-sqlite3'; +import multipart from '@fastify/multipart'; + +import { gImage } from './gImage.js'; +import { pImage } from './pImage.js'; +import { dImage } from './dImage.js'; + +const env = process.env.NODE_ENV || 'development'; + +let database; +if (!env || env === 'development') { + database = new Database(':memory:', { verbose: console.log }); +} else { + const dbPath = process.env.DB_PATH || '/db/db.sqlite' + database = new Database(dbPath); +} + +function prepareDB() { + database.exec(` + CREATE TABLE IF NOT EXISTS images ( + imageId INTEGER PRIMARY KEY AUTOINCREMENT, + fileName TEXT, + mimeType TEXT, + data BLOB + ) STRICT + `); +} + +prepareDB(); + +// POST +const postImage = database.prepare('INSERT INTO images (fileName, mimeType, data) VALUES (?, ?, ?);'); + +// GET +const getImage = database.prepare('SELECT fileName, mimeType, data FROM images WHERE imageId = ?;'); + +// DELETE +const deleteImage = database.prepare('DELETE FROM images WHERE imageId = ?;'); + +export default async function(fastify, options) { + fastify.register(fastifyJWT, { + secret: process.env.JWT_SECRET || '123456789101112131415161718192021', + cookie: { + cookieName: 'token', + }, + }); + fastify.register(fastifyCookie); + fastify.register(multipart, { limits: { fileSize: 2 * 1024 * 1024 } }); + + fastify.decorate('authenticate', async function(request, reply) { + try { + const jwt = await request.jwtVerify(); + request.user = jwt.user; + } catch (err) { + reply.code(401).send({ error: 'Unauthorized' }); + } + }); + + fastify.decorate('authenticateAdmin', async function(request, reply) { + try { + const jwt = await request.jwtVerify(); + if (jwt.user !== 'admin') { + throw ('You lack administrator privileges'); + } + } catch (err) { + reply.code(401).send({ error: 'Unauthorized' }); + } + }); + + // GET + fastify.get('/images/:imageId', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return gImage(request, reply, fastify, getImage); + }); + + // POST + fastify.post('/images', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return pImage(request, reply, fastify, postImage); + }); + + // DELETE + fastify.delete('/images/:imageId', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return dImage(request, reply, fastify, deleteImage); + }); +} diff --git a/src/api/images/gImage.js b/src/api/images/gImage.js new file mode 100644 index 0000000..abbaa42 --- /dev/null +++ b/src/api/images/gImage.js @@ -0,0 +1,13 @@ +export async function gImage(request, reply, fastify, getImage) { + try { + const imageId = Number(request.params.imageId); + const image = getImage.get(imageId); + if (!image) { + return reply.code(404).send({ error: "Image does not exist" }); + } + return reply.code(200).type(image.mimeType).header('Content-Disposition', `inline; filename="${image.fileName}"`).send(image.data); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/images/pImage.js b/src/api/images/pImage.js new file mode 100644 index 0000000..d3b930b --- /dev/null +++ b/src/api/images/pImage.js @@ -0,0 +1,33 @@ +export async function pImage(request, reply, fastify, postImage) { + try { + const parts = request.parts(); + for await (const part of parts) { + if (part.file) { + const chunks = []; + for await (const chunk of part.file) { + chunks.push(chunk); + } + const buffer = Buffer.concat(chunks); + if (!part.filename || part.filename.trim() === '') { + return reply.code(400).send({ error: "Missing filename" }); + } + if (!part.mimetype || part.mimetype.trim() === '') { + return reply.code(400).send({ error: "Missing mimetype" }); + } + const ext = part.filename.toLowerCase().substring(part.filename.lastIndexOf('.')); + if (ext !== 'webp') { + return reply.code(400).send({ error: "Wrong file extension" }); + } + // check size max here ? + // convert image to webp using sharp + //sharp(buffer, ).toFile(); + const id = postImage.run(part.filename, part.mimetype, buffer); + return reply.code(200).send({ msg: "Image uploaded successfully", imageId: id.lastInsertRowid }); + } + } + return reply.code(400).send({ error: "No file uploaded" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/TODO b/src/api/user/TODO deleted file mode 100644 index 326da16..0000000 --- a/src/api/user/TODO +++ /dev/null @@ -1,8 +0,0 @@ -Todo : -- create users with an avatar (by default) -> POST/GET/PATCH/DELETE avatar -- create a whole image upload API that ensures files are not executables, converts to a single type, stores the image and returns a UID to address them -- add a privacy setting so not anybody can GET friends, match history, etc. (what are the RGPD requirements ?) ? - - - -Always update API doc diff --git a/src/api/user/dAvatar.js b/src/api/user/dAvatar.js new file mode 100644 index 0000000..822c786 --- /dev/null +++ b/src/api/user/dAvatar.js @@ -0,0 +1,9 @@ +export async function dAvatar(request, reply, fastify, deleteAvatarId) { + try { + ; + return reply.code(200).send({ msg: "Avatar deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/default.js b/src/api/user/default.js index a2cc806..1b57c07 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -2,22 +2,25 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; import Database from 'better-sqlite3'; -import { gUsers } from './gUsers.js' -import { gUser } from './gUser.js' -import { gNumberUsers } from './gNumberUsers.js' -import { gFriends } from './gFriends.js' -import { gNumberFriends } from './gNumberFriends.js' -import { gMatchHistory } from './gMatchHistory.js' -import { gNumberMatches } from './gNumberMatches.js' -import { pUser } from './pUser.js' -import { pFriend } from './pFriend.js' -import { pMatchHistory } from './pMatchHistory.js' -import { uMember } from './uMember.js' -import { dUser } from './dUser.js' -import { dMember } from './dMember.js' -import { dFriends } from './dFriends.js' -import { dFriend } from './dFriend.js' -import { dMatchHistory } from './dMatchHistory.js' +import { gUsers } from './gUsers.js'; +import { gUser } from './gUser.js'; +import { gNumberUsers } from './gNumberUsers.js'; +import { gFriends } from './gFriends.js'; +import { gNumberFriends } from './gNumberFriends.js'; +import { gMatchHistory } from './gMatchHistory.js'; +import { gNumberMatches } from './gNumberMatches.js'; +import { pUser } from './pUser.js'; +import { pFriend } from './pFriend.js'; +import { pMatchHistory } from './pMatchHistory.js'; +import { uMember } from './uMember.js'; +import { dUser } from './dUser.js'; +import { dMember } from './dMember.js'; +import { dFriends } from './dFriends.js'; +import { dFriend } from './dFriend.js'; +import { dMatchHistory } from './dMatchHistory.js'; +import { pAvatar } from './pAvatar.js'; +import { gAvatar } from './gAvatar.js'; +import { dAvatar } from './dAvatar.js'; const env = process.env.NODE_ENV || 'development'; @@ -35,6 +38,7 @@ function prepareDB() { id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, displayName TEXT, + avatarId INTEGER, pongWins INTEGER, pongLosses INTEGER, tetrisWins INTEGER, @@ -73,16 +77,18 @@ function prepareDB() { prepareDB(); // POST -const createUser = database.prepare('INSERT INTO userData (username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses) VALUES (?, ?, 0, 0, 0, 0);'); +const createUser = database.prepare('INSERT INTO userData (username, displayName, avatarId, pongWins, pongLosses, tetrisWins, tetrisLosses) VALUES (?, ?, -1, 0, 0, 0, 0);'); const addFriend = database.prepare('INSERT INTO friends (username, friendName) VALUES (?, ?);'); const addMatch = database.prepare('INSERT INTO matchHistory (game, date, player1, player2, matchId) VALUES (?, ?, ?, ?, ?);'); const incWinsPong = database.prepare('UPDATE userData SET pongWins = pongWins + 1 WHERE username = ?;'); const incLossesPong = database.prepare('UPDATE userData SET pongLosses = pongLosses + 1 WHERE username = ?'); const incWinsTetris = database.prepare('UPDATE userData SET tetrisWins = tetrisWins + 1 WHERE username = ?;'); const incLossesTetris = database.prepare('UPDATE userData SET tetrisLosses = tetrisLosses + 1 WHERE username = ?'); +const setAvatarId = database.prepare('UPDATE userData SET avatarId = ? WHERE username = ?;'); // PATCH const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;'); +const changeAvatarId = database.prepare('UPDATE userData SET avatarId = ? WHERE username = ?;'); // GET const getUserData = database.prepare('SELECT username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses FROM userData LIMIT ? OFFSET ?;'); @@ -92,7 +98,8 @@ const getFriend = database.prepare('SELECT friendName FROM friends WHERE usernam const getMatchHistory = database.prepare('SELECT matchId, date FROM matchHistory WHERE game = ? AND ? IN (player1, player2) LIMIT ? OFFSET ?;'); const getNumberUsers = database.prepare('SELECT COUNT (DISTINCT username) AS n_users FROM userData;'); const getNumberFriends = database.prepare('SELECT COUNT (DISTINCT friendName) AS n_friends FROM friends WHERE username = ?;'); -const getNumberMatches = database.prepare('SELECT COUNT (DISTINCT id) AS n_matches FROM matchHistory WHERE game = ? AND ? IN (player1, player2);') +const getNumberMatches = database.prepare('SELECT COUNT (DISTINCT id) AS n_matches FROM matchHistory WHERE game = ? AND ? IN (player1, player2);'); +const getAvatarId = database.prepare('SELECT avatarId FROM userData WHERE username = ?;'); // DELETE const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;'); @@ -101,6 +108,7 @@ const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;' const deleteMatchHistory = database.prepare('DELETE FROM matchHistory WHERE game = ? AND ? IN (player1, player2);'); const deleteStatsPong = database.prepare('UPDATE userData SET pongWins = 0, pongLosses = 0 WHERE username = ?;'); const deleteStatsTetris = database.prepare('UPDATE userData SET tetrisWins = 0, tetrisLosses = 0 WHERE username = ?;'); +const deleteAvatarId = database.prepare('UPDATE userData SET avatarId = -1 WHERE username = ?;'); const querySchema = { type: 'object', required: ['iStart', 'iEnd'], properties: { iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } } const bodySchema = { type: 'object', required: ['opponent', 'myScore', 'opponentScore'], properties: { opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } } @@ -159,6 +167,9 @@ export default async function(fastify, options) { fastify.get('/users/:userId/matchHistory/count', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => { return gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches); }); + fastify.get('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return gAvatar(request, reply, fastify, getAvatarId); + }); // POST fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => { @@ -170,10 +181,13 @@ export default async function(fastify, options) { fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMatchHistory } }, async (request, reply) => { return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris); }); + fastify.post('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return pAvatar(request, reply, fastify, setAvatarId); + }); // PATCH fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate] }, async (request, reply) => { - return uMember(request, reply, fastify, getUserInfo, changeDisplayName); + return uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId); }); // DELETE @@ -192,4 +206,7 @@ export default async function(fastify, options) { fastify.delete('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => { return dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris); }); + fastify.delete('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return dAvatar(request, reply, fastify, deleteAvatarId); + }); } diff --git a/src/api/user/gAvatar.js b/src/api/user/gAvatar.js new file mode 100644 index 0000000..2323220 --- /dev/null +++ b/src/api/user/gAvatar.js @@ -0,0 +1,9 @@ +export async function gAvatar(request, reply, fastify, getAvatarId) { + try { + ; + return reply.code(200).send({ }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pAvatar.js b/src/api/user/pAvatar.js new file mode 100644 index 0000000..e7e9db6 --- /dev/null +++ b/src/api/user/pAvatar.js @@ -0,0 +1,13 @@ +export async function pAvatar(request, reply, fastify, setAvatarId) { + try { +/* const res = await fetch('http://localhost:3004/images', { method: "POST", headers: { } }); + if (!res.ok) { + return reply.code(500).send({ error: "Internal server error" }); + } + const data = await res.json();*/ + return reply.code(200).send({ msg: "Avatar uploaded successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/uMember.js b/src/api/user/uMember.js index 67997d7..01c1dc5 100644 --- a/src/api/user/uMember.js +++ b/src/api/user/uMember.js @@ -1,4 +1,4 @@ -export async function uMember(request, reply, fastify, getUserInfo, changeDisplayName) { +export async function uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId) { try { const userId = request.params.userId; if (!request.user) { diff --git a/src/start.js b/src/start.js index df7bc0e..c99534d 100644 --- a/src/start.js +++ b/src/start.js @@ -1,6 +1,7 @@ import Fastify from 'fastify'; import authApi from './api/auth/default.js'; import userApi from './api/user/default.js'; +import imagesApi from './api/images/default.js'; import scoreApi from './api/scoreStore/default.js'; import fs from 'fs'; import path from 'path'; @@ -68,6 +69,16 @@ async function start() { servers.push(score); } + if (target === 'images' || target === 'all') { + const images = Fastify({ logger: loggerOption('images') }); + images.register(imagesApi); + const port = target === 'all' ? 3004 : 3000; + const host = target === 'all' ? '127.0.0.1' : '0.0.0.0'; + await images.listen({ port, host }); + console.log(`Images API listening on http://${host}:${port}`); + servers.push(images); + } + // Graceful shutdown on SIGINT process.on('SIGINT', async () => { console.log('SIGINT received, closing servers...');