diff --git a/Justfile b/Justfile index a9024ab..ad2eb32 100644 --- a/Justfile +++ b/Justfile @@ -55,4 +55,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/README.md b/README.md index 9d75505..24e7168 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Press F to pay respect │ └── volumes.yml # Docker volume definitions ├── src/ # Application source code │ ├── api/ # Backend logic (auth, user management) +│ ├── contract/ # Smart contract files │ ├── front/ # Frontend files │ └── utils/ # Utility modules (auth, TOTP, etc.) ├── flake.nix & flake.lock # Nix flake configuration @@ -18,44 +19,44 @@ Press F to pay respect ``` ## Modules done -6 major + 2 minor = 7 full modules +8 major + 4 minor = 10 full modules - **Web** - [x] Use a framework to build the backend.(node with Fastify) - Major - - [ ] Use a framework or toolkit to build the front-end.(Tailwind CSS) - Minor + - [x] Use a framework or toolkit to build the front-end.(Tailwind CSS) - Minor - [x] Use a database for the backend -and more.(SQLite) - Minor - [x] Store the score of a tournament in the Blockchain.(Soldity on Avalanche) - Major - **User Management** - - [ ] Standard user management, authentication and users across tournaments. - Major + - [x] Standard user management, authentication and users across tournaments. - Major - [x] Implement remote authentication. - Major - **Gameplay and user experience** - - [ ] Remote players - Major - - [ ] Multiplayer - Major - - [ ] Add another game - Major - - [ ] Game customization options - Minor - - [ ] Live chat - Major + - [ ] ~~Remote players - Major~~ + - [ ] ~~Multiplayer - Major~~ + - [x] Add another game - Major + - [ ] ~~Game customization options - Minor~~ + - [ ] ~~Live chat - Major~~ - **AI-Algo** - - [ ] AI opponent - Major + - [ ] ~~AI opponent - Major~~ - [ ] User and game stats dashboards - Minor - **Cybersecurity** - - [ ] WAF/ModSecurity and Hashicorp Vault - Major - - [ ] RGPD compliance - Minor + - [ ] ~~WAF/ModSecurity and Hashicorp Vault - Major~~ + - [ ] ~~RGPD compliance - Minor~~ - [x] 2FA and JWT - Major - **DevOps** - [x] Infrasctructure setup for log management - Major - [x] Monitoring system - Minor - [x] Designing the backend in micro-architecture - Major - **Graphics** - - [ ] Use of advanced 3D techniques - Major + - [ ] ~~Use of advanced 3D techniques - Major~~ - **Accessibility** - - [ ] Support on all devices - Minor - - [ ] Expanding Browser compatibility - Minor - - [ ] Multiple language support - Minor - - [ ] Add accessibility for visually impaired users - Minor - - [ ] Server-Side Rendering (SSR) integration - Minor + - [ ] ~~Support on all devices - Minor~~ + - [x] Expanding Browser compatibility - Minor + - [ ] ~~Multiple language support - Minor~~ + - [ ] ~~Add accessibility for visually impaired users - Minor~~ + - [ ] ~~Server-Side Rendering (SSR) integration - Minor~~9 - **Server-Side Pong** - - [ ] Replace basic pong with server-side pong and implementing an API - Major - - [ ] Enabling pong gameplay via CLI against web users with API integration - Major + - [ ] ~~Replace basic pong with server-side pong and implementing an API - Major~~ + - [ ] ~~Enabling pong gameplay via CLI against web users with API integration - Major~~ ## License diff --git a/doc/auth/login.md b/doc/auth/login.md index 76945d3..eff517d 100644 --- a/doc/auth/login.md +++ b/doc/auth/login.md @@ -21,7 +21,8 @@ Input needed : ```json { "user": "", - "password": "" + "password": "", + (optional)"token": "<2fa token>" } ``` @@ -32,7 +33,13 @@ Can return: "msg": "Login successfully" } ``` -- 400 with response +- 402 with response +```json +{ + "msg": "Please specify a 2fa token" +} +``` +- 400 || 401 with response ```json { "error": "" diff --git a/doc/auth/remove.md b/doc/auth/remove.md new file mode 100644 index 0000000..366c89c --- /dev/null +++ b/doc/auth/remove.md @@ -0,0 +1,32 @@ +# remove user + +Available endpoints: +- DELETE `/` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## DELETE `/` + +User to remove a user from the backend + +Inputs: just need a valid JWT cookie + +Returns: +- 200 +```json +{ + "msg": "User successfully deleted" +} +``` +- 401 || 400 +```json +{ + "error": " +} +``` diff --git a/doc/user/avatar.md b/doc/user/avatar.md new file mode 100644 index 0000000..d3232e4 --- /dev/null +++ b/doc/user/avatar.md @@ -0,0 +1,120 @@ +# Avatar + +Available endpoints: +- POST `/users/:userId/avatar` +- GET `/users/:userId/avatar` +- PATCH `/users/:userId/avatar` +- DELETE `/users/:userId/avatar` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST /users/:userId/avatar + +Used to upload an avatar + +Input needed : +```json +{ + +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Avatar uploaded successfully" +} +``` +- 400 with response (if the file is too large, or file is missing, or it is missing a file name, or it is missing a mime type) +```json +{ + "error": "" +} +``` +- 404 with response (if the user does not exist) +```json +{ + "error": "" +} +``` + +## GET /users/:userId/avatar + +Used to download an avatar + +Input needed : +```json +{ + +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Avatar uploaded successfully" +} +``` +- 404 with response (if the user does not exist, or the user does not have an assigned avatar, or the image does not exist) +```json +{ + "error": "" +} +``` + +## PATCH /users/:userId/avatar + +Used to modify an avatar + +Input needed : +```json +{ + +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Avatar modified successfully" +} +``` +- 400 with response (if the file is too large, or file is missing, or it is missing a file name, or it is missing a mime type) +```json +{ + "error": "" +} +``` +- 404 with response (if the user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE /users/:userId/avatar + +Used to delete an avatar + +Can return: +- 200 with response +```json +{ + "msg": "Avatar deleted successfully" +} +``` +- 404 with response (if the user does not exist, or the user does not have an assigned avatar) +```json +{ + "error": "" +} +``` diff --git a/doc/user/friend.md b/doc/user/friend.md new file mode 100644 index 0000000..377af5c --- /dev/null +++ b/doc/user/friend.md @@ -0,0 +1,155 @@ +# Friend + +Available endpoints: +- POST `/users/:userId/friends` +- GET `/users/:userId/friends` +- GET `/users/:userId/friends/count` +- DELETE `/users/:userId/friends` +- DELETE `/users/:userId/friends/:friendId` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/users/:userId/friends/:friendId` + +Used to add a friend to an user + +Can return: +- 200 with response +```json +{ + "msg": "Friend added successfully" +} +``` +- 400 with response (if no user is specified in header, or friend is the user specified in header, or friend is already added) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or friend does not exist) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/friends?iStart=&iEnd=` + +Used to get the friends of an user + +Can return: +- 200 with response (list of friend objects (between iStart and iEnd)) +```json +{ + "friends": + [ + { + "friendName": "", + "friendDisplayName": "" + }, + ... + ] +} +``` +- 400 with response (if iStart/iEnd is missing, or iEnd < iStart) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or no friends exist in the selected range) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/friends/count` + +Used to get the number of friends of an user + +Can return: +- 200 with response +```json +{ + "n_friends": +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/friends` + +Used to delete the friends of an user + +Can return: +- 200 with response +```json +{ + "msg": "Friends deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/friends/:friendId` + +Used to delete a friend of an user + +Can return: +- 200 with response +```json +{ + "msg": "Friend deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or friend does not exist) +```json +{ + "error": "" +} +``` diff --git a/doc/user/matchHistory.md b/doc/user/matchHistory.md new file mode 100644 index 0000000..6145a8d --- /dev/null +++ b/doc/user/matchHistory.md @@ -0,0 +1,148 @@ +# Match History + +Available endpoints: +- POST `/users/:userId/matchHistory` +- GET `/users/:userId/matchHistory` +- GET `/users/:userId/matchHistory/count` +- DELETE `/users/:userId/matchHistory` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/users/:userId/matchHistory?game=` + +Used to add a match result to an user to a specific game + +Input needed : +```json +{ + "game": "" + "opponent": "", <= item only present if the match involved 2 players + "myScore": , + "opponentScore": , <= item only present if the match involved 2 players + "date": +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Match successfully saved to the blockchain" +} +``` +- 400 with response (if no user is specified in header, or no opponent/p1Score/p2Score is specified in body, or opponent is the user specified in header, or a score is negative, or the game specified is invalid, or the game should involve more players than was specified) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or opponent does not exist) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/matchHistory?game=&iStart=&iEnd=` + +Used to get the match history of an user for a specific game + +Can return: +- 200 with response (list of matches results (between iStart and iEnd)) +```json +{ + "matchHistory": + [ + { + "score": + { + "p1": "", + "p2": "", <= item only present if the match involved 2 players + "p1Score": "", + "p2Score": "", <= item only present if the match involved 2 players + "date": + }, + "tx": "" + }, + ... + ] +} +``` +- 400 with response (if iStart/iEnd does not exist, or iEnd < iStart, or the game specified is invalid) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or no matches exist in the selected range) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/matchHistory/count?game=` + +Used to get the number of matches an user played for a specific game + +Can return: +- 200 with response +```json +{ + "n_matches": +} +``` +- 400 with response (if game does not exist) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/matchHistory?game=` + +Used to delete the match history of an user for a specific game + +Can return: +- 200 with response +```json +{ + "msg": "Match history deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user, or the game specified is invalid) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` diff --git a/doc/user/ping.md b/doc/user/ping.md new file mode 100644 index 0000000..1e6dded --- /dev/null +++ b/doc/user/ping.md @@ -0,0 +1,41 @@ +# ping + +Available endpoints: +- POST `/ping` +- GET `/ping/:userId` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/ping` + +Used to send a ping and update the lastSeenTime (can be used for activity time) + +Input needed : just need a valid token + +Can return: +- 200 +```json +{ + "msg": "last seen time updated successfully" +} +``` + +## GET `/ping/:userId` + +Used to retrive the lastSeenTime of a user + +Input needed : just need a valid token + +Can return: +- 200 +```json +{ + "isLogged": "" +} +``` diff --git a/doc/user/user.md b/doc/user/user.md new file mode 100644 index 0000000..1f57d58 --- /dev/null +++ b/doc/user/user.md @@ -0,0 +1,212 @@ +# User + +Available endpoints: +- POST `/users/:userId` +- GET `/users` +- GET `/users/count` +- GET `/users/:userId` +- PATCH `/users/:userId/:member` +- DELETE `/users/:userId` +- DELETE `/users/:userId/:member` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/users/:userId` + +Used to create an user + +Input needed : +```json +{ + "displayName": "" +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "User created successfully" +} +``` +- 400 with response (if no user is specified in header, or user already exists, or no display name is specified in body) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is not admin) +```json +{ + "error": "" +} +``` + +## GET `/users?iStart=&iEnd=` + +Used to get the list of users + +Can return: +- 200 with response (list of user objects (between iStart and iEnd)) +```json +{ + "users": + [ + { + "username": "", + "displayName": "", + "pong": { + "wins": , + "losses": + }, + "tetris": { + "wins": , + "losses": + } + }, + ... + ] +} +``` +- 400 with response (if iStart/iEnd is missing, or iEnd < iStart) +```json +{ + "error": "" +} +``` +- 404 with response (if no users exist in the selected range) +```json +{ + "error": "" +} +``` + +## GET `/users/count` + +Used to get the number of users + +Always returns: +- 200 with response +```json +{ + "n_users": +} +``` + +## GET `/users/:userId` + +Used to get an user + +Can return: +- 200 with response (an user object) +```json +{ + "username": "", + "displayName": "", + "pong": { + "wins": , + "losses": + }, + "tetris": { + "wins": , + "losses": + } +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## PATCH `/users/:userId/:member` + +Used to modify a member of an user (only displayName can be modified) + +Input needed : +```json +{ + "": "" +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "<:member> modified sucessfully" +} +``` +- 400 with response (if no user is specified in header, or new value of member to modify is not provided in the body, or member does not exist) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is not admin) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId` + +Used to delete an user + +Can return: +- 200 with response +```json +{ + "msg": "User deleted successfully" +} +``` +- 404 with response (user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/:member` + +Used to delete a member of an user (only displayName can be deleted) + +Can return: +- 200 with response +```json +{ + "msg": "<:member> deleted successfully" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 400 with response (if no user is specified in header, or member to delete does not exist) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` diff --git a/package.json b/package.json index 93287eb..77c60ce 100644 --- a/package.json +++ b/package.json @@ -14,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 0a5a097..ce70826 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,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) @@ -91,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'} @@ -289,6 +295,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'} @@ -759,6 +891,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'} @@ -1458,6 +1594,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==} @@ -1673,6 +1813,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 @@ -1803,6 +1948,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 @@ -2194,6 +2427,8 @@ snapshots: detect-libc@2.0.4: {} + detect-libc@2.1.2: {} + dotenv-expand@10.0.0: {} dotenv@16.6.1: {} @@ -2925,6 +3160,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/pnpm-workspace.yaml b/pnpm-workspace.yaml index a8eee40..8a8cca4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,3 +5,4 @@ ignoredBuiltDependencies: onlyBuiltDependencies: - better-sqlite3 + - sharp diff --git a/src/api/auth/default.js b/src/api/auth/default.js index 56ebf62..ecba601 100644 --- a/src/api/auth/default.js +++ b/src/api/auth/default.js @@ -1,7 +1,8 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; -import cors from '@fastify/cors' +import cors from '@fastify/cors'; +import { totpCheck } from './totpCheck.js'; import { register } from './register.js'; import { login } from './login.js'; import { gRedir } from './gRedir.js'; @@ -12,6 +13,7 @@ import { totpSetup } from './totpSetup.js'; import { totpDelete } from './totpDelete.js'; import { totpVerify } from './totpVerify.js'; import { logout } from './logout.js'; +import { remove } from './remove.js'; const saltRounds = 10; export const appName = process.env.APP_NAME || 'knl_meowscendence'; @@ -25,9 +27,9 @@ authDB.prepareDB(); export default async function(fastify, options) { fastify.register(cors, { - origin: process.ENV.CORS_ORIGIN || 'http://localhost:5173', + origin: process.env.CORS_ORIGIN || 'http://localhost:5173', credentials: true, - methods: [ "GET", "POST", "DELETE", "OPTIONS" ] + methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"] }); fastify.register(fastifyJWT, { @@ -53,6 +55,9 @@ export default async function(fastify, options) { fastify.get('/me', { preHandler: [fastify.authenticate] }, async (request, reply) => { return { user: request.user }; }); + fastify.get('/2fa', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return totpCheck(request, reply); + }); // GOOGLE sign in fastify.get('/login/google', async (request, reply) => { @@ -117,4 +122,6 @@ export default async function(fastify, options) { }, async (request, reply) => { return register(request, reply, saltRounds, fastify); }); fastify.get('/logout', {}, async (request, reply) => { return logout(reply, fastify); }) + + fastify.delete('/', { preHandler: fastify.authenticate }, async (request, reply) => { return remove(request, reply, fastify) }) } diff --git a/src/api/auth/login.js b/src/api/auth/login.js index 2580383..83d9a76 100644 --- a/src/api/auth/login.js +++ b/src/api/auth/login.js @@ -37,8 +37,8 @@ export async function login(request, reply, fastify) { const userTOTP = authDB.getUser(user); if (userTOTP.totpEnabled == 1) { - if (!request.body.token){ - return reply.code(401).send({ error: 'Invalid 2FA token' }); + if (!request.body.token) { + return reply.code(402).send({ error: 'Please specify a 2fa token' }); } const isValid = verifyTOTP(userTOTP.totpHash, request.body.token); if (!isValid) { diff --git a/src/api/auth/remove.js b/src/api/auth/remove.js new file mode 100644 index 0000000..ea1cbce --- /dev/null +++ b/src/api/auth/remove.js @@ -0,0 +1,35 @@ +import authDB from '../../utils/authDB.js'; +import { authUserRemove } from '../../utils/authUserRemove.js'; + +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} reply + * @param {import('fastify').FastifyInstance} fastify + */ +export async function remove(request, reply, fastify) { + try { + const user = request.user; + + if (authDB.RESERVED_USERNAMES.includes(user)) { + return reply.code(400).send({ error: 'Reserved username' }); + } + + if (authDB.checkUser(user) === false) { + return reply.code(400).send({ error: "User does not exist" }); + } + + authDB.rmUser(user) + + authUserRemove(user, fastify) + + return reply + .code(200) + .clearCookie("token") + .send({ + msg: "User successfully deleted" + }) + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/auth/totpCheck.js b/src/api/auth/totpCheck.js new file mode 100644 index 0000000..d76a12e --- /dev/null +++ b/src/api/auth/totpCheck.js @@ -0,0 +1,24 @@ +import authDB from '../../utils/authDB.js'; + +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} reply + */ +export async function totpCheck(request, reply) { + try { + const user = request.user; + + if (authDB.checkUser(user) === false) { + return reply.code(400).send({ error: "User does not exist" }); + } + + return reply + .code(200) + .send({ + totp: authDB.isTOTPEnabled(user) + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/scoreStore/addTx.js b/src/api/scoreStore/addTx.js index 413086b..e9682fa 100644 --- a/src/api/scoreStore/addTx.js +++ b/src/api/scoreStore/addTx.js @@ -9,12 +9,11 @@ import { callAddScore, callLastId } from "../../utils/scoreStore_contract.js"; */ export async function addTx(request, reply, fastify) { try { - const id = await callLastId(); - const tx = callAddScore(request.body.p1, request.body.p2, request.body.p1Score, request.body.p2Score); + const {tx, id} = await callAddScore(request.body.p1, request.body.p2, request.body.p1Score, request.body.p2Score); - tx.then(tx => { - scoreDB.addTx(id, tx.hash); - }); + scoreDB.addTx(id, tx.hash); + // tx.then(tx => { + // }); return reply.code(200).send({ id: Number(id) diff --git a/src/api/scoreStore/default.js b/src/api/scoreStore/default.js index 954fcc8..c6b39ea 100644 --- a/src/api/scoreStore/default.js +++ b/src/api/scoreStore/default.js @@ -20,7 +20,7 @@ export default async function(fastify, options) { required: ['p1', 'p2', 'p1Score', 'p2Score'], properties: { p1: { type: 'string', minLength: 1 }, - p2: { type: 'string', minLength: 1 }, + p2: { type: 'string', minLength: 0 }, p1Score: { type: 'integer', minimum: 0 }, p2Score: { type: 'integer', minimum: 0 }, } diff --git a/src/api/user/dAvatar.js b/src/api/user/dAvatar.js new file mode 100644 index 0000000..cfbea1c --- /dev/null +++ b/src/api/user/dAvatar.js @@ -0,0 +1,24 @@ +export async function dAvatar(request, reply, fastify, getUserInfo, getAvatarId, deleteAvatarId, deleteImage) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + if (!getUserInfo.get(userId)) { + return reply.cose(404).send({ error: "User does not exist" }); + } + const imageId = getAvatarId.get(userId); + if (imageId.avatarId === -1) { + return reply.code(404).send({ error: "User does not have an avatar" }); + } + deleteImage.run(imageId.avatarId); + deleteAvatarId.run(userId); + 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/dFriend.js b/src/api/user/dFriend.js new file mode 100644 index 0000000..1f3fddd --- /dev/null +++ b/src/api/user/dFriend.js @@ -0,0 +1,23 @@ +export async function dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + if (request.user !== 'admin' && request.user !== userId) { + return reply.code(401).send({ error: "Unauthorized" }); + } + const friendId = request.params.friendId; + if (!getFriend.get(userId, friendId)) { + return reply.code(404).send({ error: "Friend does not exist" }); + } + deleteFriend.run(userId, friendId); + return reply.code(200).send({ msg: "Friend deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dFriends.js b/src/api/user/dFriends.js new file mode 100644 index 0000000..11d4423 --- /dev/null +++ b/src/api/user/dFriends.js @@ -0,0 +1,16 @@ +export async function dFriends(request, reply, fastify, getUserInfo, deleteFriends) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + deleteFriends.run(userId); + return reply.code(200).send({ msg: "Friends deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dMatchHistory.js b/src/api/user/dMatchHistory.js new file mode 100644 index 0000000..241813b --- /dev/null +++ b/src/api/user/dMatchHistory.js @@ -0,0 +1,26 @@ +export async function dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const { game } = request.query; + if (game !== 'pong' && game !== 'tetris') { + return reply.code(400).send({ error: "Specified game does not exist" }); + } + deleteMatchHistory.run(game, userId); + if (game === 'pong') { + deleteStatsPong.run(userId); + } + else if (game === 'tetris') { + deleteStatsTetris.run(userId); + } + return reply.code(200).send({ msg: "Match history deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dMember.js b/src/api/user/dMember.js new file mode 100644 index 0000000..32630b9 --- /dev/null +++ b/src/api/user/dMember.js @@ -0,0 +1,22 @@ +export async function dMember(request, reply, fastify, getUserInfo, changeDisplayName) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const user = request.user; + const member = request.params.member; + if (member === 'displayName') { + changeDisplayName.run("", request.params.userId); + return reply.code(200).send({ msg: "Display name deleted successfully" }); + } else { + return reply.code(400).send({ msg: "Member does not exist" }) + } + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dUser.js b/src/api/user/dUser.js new file mode 100644 index 0000000..fad5571 --- /dev/null +++ b/src/api/user/dUser.js @@ -0,0 +1,19 @@ +export async function dUser(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteFriends, deleteUser) { + try { + if (request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + deleteMatchHistory.run('pong', userId); + deleteMatchHistory.run('tetris', userId); + deleteFriends.run(userId); + deleteUser.run(userId); + return reply.code(200).send({ msg: "User 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 2c7902b..0057207 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -2,59 +2,151 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; import cors from '@fastify/cors' import Database from 'better-sqlite3'; +import cors from '@fastify/cors'; -var env = process.env.NODE_ENV || 'development'; +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 { uAvatar } from './uAvatar.js'; +import { dAvatar } from './dAvatar.js'; +import { pPing } from './pPing.js'; +import { gPing } from './gPing.js'; + +const env = process.env.NODE_ENV || 'development'; let database; - if (!env || env === 'development') { - database = new Database(":memory:", { verbose: console.log }); + database = new Database(':memory:', { verbose: console.log }); } else { - var dbPath = process.env.DB_PATH || '/db/db.sqlite' + const dbPath = process.env.DB_PATH || '/db/db.sqlite' database = new Database(dbPath); } function prepareDB() { database.exec(` CREATE TABLE IF NOT EXISTS userData ( - username TEXT PRIMARY KEY, - displayName TEXT + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT, + displayName TEXT, + avatarId INTEGER, + pongWins INTEGER, + pongLosses INTEGER, + tetrisWins INTEGER, + tetrisLosses INTEGER, + UNIQUE(username), + CHECK(pongWins >= 0), + CHECK(pongLosses >= 0), + CHECK(tetrisWins >= 0), + CHECK(tetrisLosses >= 0) ) STRICT `); database.exec(` CREATE TABLE IF NOT EXISTS friends ( + id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, friendName TEXT, UNIQUE(username, friendName), CHECK(username != friendName) - ) + ) STRICT + `); + database.exec(` + CREATE TABLE IF NOT EXISTS matchHistory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + game TEXT, + date INTEGER, + player1 TEXT, + player2 TEXT, + matchId INTEGER, + CHECK(game = 'pong' OR game = 'tetris'), + CHECK(date >= 0), + CHECK(player1 != player2) + ) STRICT + `); + database.exec(` + CREATE TABLE IF NOT EXISTS activityTime ( + username TEXT PRIMARY KEY, + time TEXT + ) STRICT + `); + database.exec(` + CREATE TABLE IF NOT EXISTS images ( + imageId INTEGER PRIMARY KEY AUTOINCREMENT, + fileName TEXT, + mimeType TEXT, + data BLOB + ) STRICT `); } prepareDB(); // POST -const createUser = database.prepare('INSERT INTO userData (username, displayName) VALUES (?, ?);'); +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 = ?;'); +const postImage = database.prepare('INSERT INTO images (fileName, mimeType, data) VALUES (?, ?, ?);'); +const setActivityTime = database.prepare(` + INSERT INTO activityTime (username, time) + VALUES (?, ?) + ON CONFLICT(username) DO UPDATE SET time = excluded.time; +`); // PATCH const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;'); +const changeAvatarId = database.prepare('UPDATE userData SET avatarId = ? WHERE username = ?;'); // GET -const getUserInfo = database.prepare('SELECT * FROM userData WHERE username = ?;'); -const getUserData = database.prepare('SELECT * FROM userData;'); -const getFriends = database.prepare('SELECT friendName FROM friends WHERE username = ?;'); -// const isFriend = database.prepare('SELECT 1 FROM friends WHERE username = ? AND friendName = ?;'); +const getUserData = database.prepare('SELECT username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses FROM userData LIMIT ? OFFSET ?;'); +const getUserInfo = database.prepare('SELECT username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses FROM userData WHERE username = ?;'); +const getFriends = database.prepare('SELECT friendName FROM friends WHERE username = ? LIMIT ? OFFSET ?;'); +const getFriend = database.prepare('SELECT friendName FROM friends WHERE username = ? AND friendName = ?;'); +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 getAvatarId = database.prepare('SELECT avatarId FROM userData WHERE username = ?;'); +const getImage = database.prepare('SELECT fileName, mimeType, data FROM images WHERE imageId = ?;'); +const getActivityTime = database.prepare('SELECT time FROM activityTime WHERE username = ?;') // DELETE const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;'); const deleteFriend = database.prepare('DELETE FROM friends WHERE username = ? AND friendName = ?;'); 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 deleteImage = database.prepare('DELETE FROM images WHERE imageId = ?;'); +const querySchema = { type: 'object', required: ['iStart', 'iEnd'], properties: { iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } }; +const bodySchemaMember = { type: 'object', properties: { displayName: { type: 'string' } } }; +const querySchemaMatchHistory = { type: 'object', required: ['game', 'iStart', 'iEnd'], properties: { game: { type: 'string' }, iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } }; +const bodySchemaMatchHistory = { type: 'object', required: ['game', 'date', 'myScore'], properties: { game: { type: 'string' }, date: { type: 'integer', minimum: 0 }, opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } }; +const querySchemaMatchHistoryGame = { type: 'object', required: ['game'], properties: { game: { type: 'string' } } }; /** - * @param {import('fastify').FastifyInstance} fastify - * @param {import('fastify').FastifyPluginOptions} options + * @param {import('fastify').FastifyInstance} fastify + * @param {import('fastify').FastifyPluginOptions} options */ export default async function(fastify, options) { @@ -71,8 +163,19 @@ export default async function(fastify, options) { }, }); fastify.register(fastifyCookie); + fastify.register(cors, { + origin: process.env.CORS_ORIGIN || 'http://localhost:5173', + credentials: true, + methods: [ "GET", "POST", "PATCH", "DELETE", "OPTIONS" ] + }); - fastify.decorate("authenticate", async function(request, reply) { + fastify.addContentTypeParser( + ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], + { parseAs: 'buffer' }, + async (request, payload) => payload + ); + + fastify.decorate('authenticate', async function(request, reply) { try { const jwt = await request.jwtVerify(); request.user = jwt.user; @@ -81,178 +184,89 @@ export default async function(fastify, options) { } }); - fastify.decorate("authenticateAdmin", async function(request, reply) { + fastify.decorate('authenticateAdmin', async function(request, reply) { try { const jwt = await request.jwtVerify(); if (jwt.user !== 'admin') { - throw (""); + throw ('You lack administrator privileges'); } + request.user = jwt.user; } catch (err) { reply.code(401).send({ error: 'Unauthorized' }); } }); // GET - fastify.get('/users', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const users = getUserData.all(); - - return reply.code(200).send({ users }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + fastify.get('/users', { preHandler: [fastify.authenticate], schema: { querystring: querySchema } }, async (request, reply) => { + return gUsers(request, reply, fastify, getUserData); + }); + fastify.get('/users/count', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return gNumberUsers(request, reply, fastify, getNumberUsers); }); fastify.get('/users/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const info = getUserInfo.get(request.params.userId); - - return reply.code(200).send({ info }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + return gUser(request, reply, fastify, getUserInfo); }); - fastify.get('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const userId = request.params.userId; - - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - - if (userId == request.user || request.user == 'admin') { - const friends = getFriends.all(userId); - - if (!friends) { - return reply.code(404).send({ error: "User does not have friends D:" }); - } - return reply.code(200).send({ friends }); - } - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + fastify.get('/users/:userId/friends', { preHandler: [fastify.authenticate], schema: { querystring: querySchema } }, async (request, reply) => { + return gFriends(request, reply, fastify, getUserInfo, getFriends); + }); + fastify.get('/users/:userId/friends/count', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return gNumberFriends(request, reply, fastify, getUserInfo, getNumberFriends); + }); + fastify.get('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { querystring: querySchemaMatchHistory } }, async (request, reply) => { + return gMatchHistory(request, reply, fastify, getUserInfo, getMatchHistory); + }); + 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, getUserInfo, getAvatarId, getImage); + }); + fastify.get('/ping/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return gPing(request, reply, fastify, getActivityTime); }); // POST fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => { - try { - const userId = request.params.userId; - - if (getUserInfo.get(userId)) { - return reply.code(400).send({ error: "User already exist" }); - } - createUser.run(userId, userId); - return reply.code(200).send({ msg: "User created sucessfully" }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } - }) - fastify.post('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const userId = request.params.userId; - if (request.user != 'admin' && request.user != userId) { - return reply.code(401).send({ error: "Unauthorized" }); - } - if (!request.body || !request.body.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - if (!getUserInfo.get(request.body.user)) { - return reply.code(404).send({ error: "Friend does not exist" }); - } - if (request.body.user === userId) { - return reply.code(400).send({ error: "You can't add yourself :D" }); - } - addFriend.run(userId, request.body.user) - return reply.code(200).send({ msg: "Friend added sucessfully" }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + return pUser(request, reply, fastify, getUserInfo, createUser); }); + fastify.post('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return pFriend(request, reply, fastify, getUserInfo, getFriend, addFriend); + }); + 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', { bodyLimit: 5242880, preHandler: [fastify.authenticate] }, async (request, reply) => { + return pAvatar(request, reply, fastify, getUserInfo, setAvatarId, postImage); + }); + fastify.post('/ping', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return pPing(request, reply, fastify, setActivityTime); + }) // PATCH - fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const userId = request.params.userId; - if (request.user != 'admin' && request.user != userId) { - return reply.code(401).send({ error: "Unauthorized" }); - } - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - const member = request.params.member; - - if (member === 'displayName') { - if (!request.body || !request.body.displayName) { - return reply.code(400).send({ error: "Please specify a displayName" }); - } - - changeDisplayName.run(request.body.displayName, userId); - return reply.code(200).send({ msg: "displayName modified sucessfully" }); - } - return reply.code(400).send({ error: "Member does not exist" }) - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } - }) + fastify.patch('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return uAvatar(request, reply, fastify, getUserInfo, setAvatarId, getAvatarId, deleteAvatarId, postImage, deleteImage); + }); + fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMember } }, async (request, reply) => { + return uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId); + }); // DELETE - /** - * @description Can be used to delete a user from the db - */ fastify.delete('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => { - try { - if (!getUserInfo(request.params.userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - deleteUser.run(request.params.userId); - deleteFriends.run(request.params.userId); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + return dUser(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteFriends, deleteUser); }); fastify.delete('/users/:userId/:member', { preHandler: fastify.authenticate }, async (request, reply) => { - try { - const user = request.user; - const member = request.params.member; - if (user == 'admin' || user == request.params.userId) { - if (member == 'displayName') { - changeDisplayName.run("", request.params.userId); - return reply.code(200).send({ msg: "displayName cleared sucessfully" }); - } - return reply.code(400).send({ msg: "member does not exist" }) - } else { - return reply.code(401).send({ error: 'You dont have the right to delete this' }); - } - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } - + return dMember(request, reply, fastify, getUserInfo, changeDisplayName); + }); + fastify.delete('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return dFriends(request, reply, fastify, getUserInfo, deleteFriends); }); fastify.delete('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const userId = request.params.userId; - const friendId = request.params.friendId; - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - if (request.user != 'admin' && request.user != userId) { - return reply.code(401).send({ error: "Unauthorized" }); - } - deleteFriend.run(userId, friendId); - return reply.code(200).send({ msg: "Friend remove sucessfully" }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + return dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend); + }); + 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, getUserInfo, getAvatarId, deleteAvatarId, deleteImage); }); } diff --git a/src/api/user/gAvatar.js b/src/api/user/gAvatar.js new file mode 100644 index 0000000..f7c386f --- /dev/null +++ b/src/api/user/gAvatar.js @@ -0,0 +1,23 @@ +export async function gAvatar(request, reply, fastify, getUserInfo, getAvatarId, getImage) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const imageId = getAvatarId.get(userId); + if (imageId.avatarId === -1) { + return reply.code(404).send({ error: "User does not have an avatar" }); + } + const image = getImage.get(imageId.avatarId); + if (!image) { + return reply.code(404).send({ error: "Avatar does not exist" }); + } + return reply.code(200).type(image.mimeType).send(image.data); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gFriends.js b/src/api/user/gFriends.js new file mode 100644 index 0000000..8050e17 --- /dev/null +++ b/src/api/user/gFriends.js @@ -0,0 +1,29 @@ +export async function gFriends(request, reply, fastify, getUserInfo, getFriends) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const { iStart, iEnd } = request.query; + if (Number(iEnd) < Number(iStart)) { + return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); + } + const friendNames = getFriends.all(userId, Number(iEnd) - Number(iStart), Number(iStart)); + if (!friendNames.length) { + return reply.code(404).send({ error: "No friends exist in the selected range" }); + } + const promises = friendNames.map(async (friendName) => { + const friend = getUserInfo.get(friendName.friendName); + friendName.friendDisplayName = friend.displayName; + return friendName; + }); + const friends = await Promise.all(promises); + return reply.code(200).send({ friends }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gMatchHistory.js b/src/api/user/gMatchHistory.js new file mode 100644 index 0000000..e8d3a55 --- /dev/null +++ b/src/api/user/gMatchHistory.js @@ -0,0 +1,40 @@ +export async function gMatchHistory(request, reply, fastify, getUserInfo, getMatchHistory) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const { game, iStart, iEnd } = request.query; + if (game !== 'pong' && game !== 'tetris') { + return reply.code(400).send({ error: "Specified game does not exist" }); + } + if (Number(iEnd) < Number(iStart)) { + return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); + } + const matchHistoryId = getMatchHistory.all(game, userId, Number(iEnd) - Number(iStart), Number(iStart)); + if (!matchHistoryId.length) { + return reply.code(404).send({ error: "No matches exist in the selected range" }); + } + const promises = matchHistoryId.map(async (match) => { + const res = await fetch(`http://localhost:3003/${match.matchId}`, { method: "GET" }); + if (!res.ok) { + throw new Error('Failed to fetch item from blockchain API'); + } + const resJson = await res.json(); + resJson.score.date = match.date; + if (resJson.score.p2 === "" && resJson.score.p2Score === 0) { + delete resJson.score.p2; + delete resJson.score.p2Score; + } + return resJson; + }); + const matchHistory = await Promise.all(promises); + return reply.code(200).send({ matchHistory }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gNumberFriends.js b/src/api/user/gNumberFriends.js new file mode 100644 index 0000000..03730bf --- /dev/null +++ b/src/api/user/gNumberFriends.js @@ -0,0 +1,17 @@ +export async function gNumberFriends(request, reply, fastify, getUserInfo, getNumberFriends) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + + const row = getNumberFriends.get(userId); + return reply.code(200).send({ n_friends: row.n_friends }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gNumberMatches.js b/src/api/user/gNumberMatches.js new file mode 100644 index 0000000..a784fac --- /dev/null +++ b/src/api/user/gNumberMatches.js @@ -0,0 +1,20 @@ +export async function gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const { game } = request.query; + if (game !== 'pong' && game !== 'tetris') { + return reply.code(400).send({ error: "Specified game does not exist" }); + } + const row = getNumberMatches.get(game, userId); + return reply.code(200).send({ n_matches: row.n_matches }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gNumberUsers.js b/src/api/user/gNumberUsers.js new file mode 100644 index 0000000..b8c025d --- /dev/null +++ b/src/api/user/gNumberUsers.js @@ -0,0 +1,9 @@ +export async function gNumberUsers(request, reply, fastify, getNumberUsers) { + try { + const row = getNumberUsers.get(); + return reply.code(200).send({ n_users: row.n_users }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gPing.js b/src/api/user/gPing.js new file mode 100644 index 0000000..b14f003 --- /dev/null +++ b/src/api/user/gPing.js @@ -0,0 +1,28 @@ +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} reply + * @param {import('fastify').FastifyInstance} fastify + */ +export async function gPing(request, reply, fastify, getActivityTime) { + try { + const user = request.params.userId; + const time = getActivityTime.get(user); + + if (!time || !time.time) { + return reply.code(404).send({ error: "User not found or no activity time recorded" }); + } + + const lastSeenTime = new Date(time.time); + const now = new Date(); + const oneMinuteAgo = new Date(now.getTime() - 60000); // 60,000 ms = 1 minute + + const isActiveInLastMinute = lastSeenTime >= oneMinuteAgo; + + return reply.code(200).send({ + isLogged: isActiveInLastMinute + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gUser.js b/src/api/user/gUser.js new file mode 100644 index 0000000..78e9a71 --- /dev/null +++ b/src/api/user/gUser.js @@ -0,0 +1,24 @@ +export async function gUser(request, reply, fastify, getUserInfo) { + try { + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const userInfo = getUserInfo.get(userId); + return reply.code(200).send({ + username: userInfo.username, + displayName: userInfo.displayName, + pong: { + wins: userInfo.pongWins, + losses: userInfo.pongLosses + }, + tetris: { + wins: userInfo.tetrisWins, + losses: userInfo.tetrisLosses + } + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gUsers.js b/src/api/user/gUsers.js new file mode 100644 index 0000000..bf42f33 --- /dev/null +++ b/src/api/user/gUsers.js @@ -0,0 +1,28 @@ +export async function gUsers(request, reply, fastify, getUserData) { + try { + const { iStart, iEnd } = request.query; + if (Number(iEnd) < Number(iStart)) { + return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); + } + const users = getUserData.all(Number(iEnd) - Number(iStart), Number(iStart)); + if (!users.length) { + return reply.code(404).send({ error: "No users exist in the selected range" }); + } + const usersFormat = users.map(obj => ({ + username: obj.username, + displayName: obj.displayName, + pong: { + wins: obj.pongWins, + losses: obj.pongLosses + }, + tetris: { + wins: obj.tetrisWins, + losses: obj.tetrisLosses + } + })); + return reply.code(200).send({ usersFormat }); + } 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..f90299e --- /dev/null +++ b/src/api/user/pAvatar.js @@ -0,0 +1,45 @@ +import sharp from 'sharp'; + +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} reply + * @param {import('fastify').FastifyInstance} fastify + */ +export async function pAvatar(request, reply, fastify, getUserInfo, setAvatarId, postImage) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + + // Read the raw body as a Buffer + const buffer = request.body; + + if (!buffer) { + return reply.code(400).send({ error: "No file uploaded" }); + } + + // Check file size (5MB limit) + if (buffer.length > 5 * 1024 * 1024) { + return reply.code(400).send({ error: "File too large" }); + } + + // Convert to WebP + const webpBuffer = await sharp(buffer).toFormat('webp').toBuffer(); + + // Save the image and update the user's avatar + const mimeType = request.headers['content-type']; + const fileName = `avatar_${userId}.webp`; + const imageId = postImage.run(fileName, mimeType, webpBuffer); + + setAvatarId.run(imageId.lastInsertRowid, userId); + + 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/pFriend.js b/src/api/user/pFriend.js new file mode 100644 index 0000000..ae7d414 --- /dev/null +++ b/src/api/user/pFriend.js @@ -0,0 +1,26 @@ +export async function pFriend(request, reply, fastify, getUserInfo, getFriend, addFriend) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const friendId = request.params.friendId; + if (!getUserInfo.get(friendId)) { + return reply.code(404).send({ error: "Friend does not exist" }); + } + if (friendId === userId) { + return reply.code(400).send({ error: "You can't add yourself :D" }); + } + if (getFriend.get(userId, friendId)) { + return reply.code(400).send({ error: "Friend already added" }); + } + addFriend.run(userId, friendId) + return reply.code(200).send({ msg: "Friend added successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pMatchHistory.js b/src/api/user/pMatchHistory.js new file mode 100644 index 0000000..f722775 --- /dev/null +++ b/src/api/user/pMatchHistory.js @@ -0,0 +1,66 @@ +async function fetchSave(request, reply, userId, addMatch) { + let opponentName = ''; + let opponentScore = 0; + if (request.body.opponent && request.body.opponentScore) { + opponentName = request.body.opponent; + opponentScore = request.body.opponentScore; + } + const res = await fetch('http://localhost:3003/', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ p1: userId, p2: opponentName, p1Score: request.body.myScore, p2Score: opponentScore }) }); + if (!res.ok) { + throw new Error('Internal server error'); + } + const data = await res.json(); + addMatch.run(request.body.game, request.body.date, userId, opponentName, data.id); +} + +export async function pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + if (request.body.game !== 'pong' && request.body.game !== 'tetris') { + return reply.code(400).send({ error: "Specified game does not exist" }); + } + if (request.body.game === 'pong' && (!request.body.opponent || !request.body.opponentScore)) { + return reply.code(400).send({ error: "Game requires two players" }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + if (request.body.opponent) { + if (!getUserInfo.get(request.body.opponent)) { + return reply.code(404).send({ error: "Opponent does not exist" }); + } + if (request.body.opponent === userId) { + return reply.code(400).send({ error: "Do you have dementia ? You cannot have played a match against yourself gramps" }); + } + } + await fetchSave(request, reply, userId, addMatch); + if (request.body.game === 'pong') { + if (request.body.myScore > request.body.opponentScore) { + incWinsPong.run(userId); + incLossesPong.run(request.body.opponent); + } else if (request.body.myScore < request.body.opponentScore) { + incWinsPong.run(request.body.opponent); + incLossesPong.run(userId); + } + } + else if (request.body.game === 'tetris' && request.body.opponent && request.body.opponentScore) { + if (request.body.myScore > request.body.opponentScore) { + incWinsTetris.run(userId); + incLossesTetris.run(request.body.opponent); + } else if (request.body.myScore < request.body.opponentScore) { + incWinsTetris.run(request.body.opponent); + incLossesTetris.run(userId); + } + } + return reply.code(200).send({ msg: "Match successfully saved to the blockchain" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pPing.js b/src/api/user/pPing.js new file mode 100644 index 0000000..b7ff9fd --- /dev/null +++ b/src/api/user/pPing.js @@ -0,0 +1,21 @@ +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} request + * @param {import('fastify').Fastify} fastify + */ +export async function pPing(request, reply, fastify, setActivityTime) { + try { + const user = request.user; + const currentTime = new Date().toISOString(); + + setActivityTime.run(user, currentTime); + + return reply.code(200) + .send({ + msg: "last seen time updated successfully" + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pUser.js b/src/api/user/pUser.js new file mode 100644 index 0000000..31ce8bd --- /dev/null +++ b/src/api/user/pUser.js @@ -0,0 +1,19 @@ +export async function pUser(request, reply, fastify, getUserInfo, createUser) { + try { + const userId = request.params.userId; + if (request.user !== 'admin') { + return reply.code(401).send({ error: "Unauthorized" }); + } + if (getUserInfo.get(userId)) { + return reply.code(400).send({ error: "User already exist" }); + } + if (!request.body || !request.body.displayName) { + return reply.code(400).send({ error: "Please specify a display name" }); + } + createUser.run(userId, request.body.displayName); + return reply.code(200).send({ msg: "User created successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/uAvatar.js b/src/api/user/uAvatar.js new file mode 100644 index 0000000..344307e --- /dev/null +++ b/src/api/user/uAvatar.js @@ -0,0 +1,48 @@ +import sharp from 'sharp'; + +export async function uAvatar(request, reply, fastify, getUserInfo, setAvatarId, getAvatarId, deleteAvatarId, postImage, deleteImage) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + deleteAvatarId.run(userId); + const parts = request.parts(); + for await (const part of parts) { + if (part.file) { + let size = 0; + const chunks = []; + for await (const chunk of part.file) { + size += chunk.length; + chunks.push(chunk); + } + if (size === 5 * 1024 * 1024 + 1) { + return reply.code(400).send({ error: "File too large" }); + } + 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 webpBuffer = await sharp(buffer).toFormat('webp').toBuffer(); + const imageId = postImage.run(part.filename, part.mimetype, webpBuffer); + const oldImageId = getAvatarId.get(userId); + if (oldImageId.avatarId !== -1) { + deleteImage.run(oldImageId.avatarId); + deleteAvatarId.run(userId); + } + setAvatarId.run(imageId.lastInsertRowid, userId); + return reply.code(200).send({ msg: "Avatar modified successfully" }); + } + } + return reply.code(400).send({ error: "No avatar modified" }); + } 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 new file mode 100644 index 0000000..adf5fce --- /dev/null +++ b/src/api/user/uMember.js @@ -0,0 +1,23 @@ +export async function uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const member = request.params.member; + if (member === 'displayName') { + if (!request.body || !request.body.displayName) { + return reply.code(400).send({ error: "Please specify a displayName" }); + } + changeDisplayName.run(request.body.displayName, userId); + return reply.code(200).send({ msg: "Display name modified successfully" }); + } + return reply.code(400).send({ error: "Member does not exist" }) + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/utils/authDB.js b/src/utils/authDB.js index a4f4595..a4611f3 100644 --- a/src/utils/authDB.js +++ b/src/utils/authDB.js @@ -82,9 +82,15 @@ function getUser(user) { return stmt.get(user); } +function rmUser(user) { + const stmt = database.prepare('DELETE FROM credentials WHERE username = ?'); + stmt.run(user); +} + const authDB = { prepareDB, checkUser, + rmUser, addUser, passwordQuery, setTOTPSecret, diff --git a/src/utils/authUserRemove.js b/src/utils/authUserRemove.js new file mode 100644 index 0000000..2fc3bfc --- /dev/null +++ b/src/utils/authUserRemove.js @@ -0,0 +1,19 @@ +import axios from 'axios' + +/** + * @param {string} username + * @param {import('fastify').FastifyInstance} fastify + */ +export async function authUserRemove(username, fastify) { + const url = (process.env.USER_URL || "http://localhost:3002") + "/users/" + username; + const cookie = fastify.jwt.sign({ user: "admin" }); + + await axios.delete( + url, + { + headers: { + 'Cookie': 'token=' + cookie, + }, + } + ); +} diff --git a/src/utils/scoreStore_contract.js b/src/utils/scoreStore_contract.js index b8ada16..d9f626b 100644 --- a/src/utils/scoreStore_contract.js +++ b/src/utils/scoreStore_contract.js @@ -29,7 +29,7 @@ async function loadContract() { async function callGetScore(id) { try { const contract = await loadContract(); - const result = await contract.getScore(id); + const result = await contract.getScore(id - 1); return result; } catch (error) { console.error('Error calling view function:', error); @@ -54,8 +54,9 @@ async function callAddScore(p1, p2, p1Score, p2Score) { const tx = await contract.addScore(p1, p2, p1Score, p2Score); console.log('Transaction sent:', tx.hash); await tx.wait(); // Wait for the transaction to be mined + const id = await callLastId(); console.log('Transaction confirmed'); - return tx; + return { tx, id }; } catch (error) { console.error('Error calling addScore function:', error); throw error;