」 feat(front): merged everything, i dont have a merge commit and im lazy ok

This commit is contained in:
y-syo
2025-10-23 23:08:36 +02:00
51 changed files with 2173 additions and 225 deletions

View File

@ -55,4 +55,4 @@ set dotenv-load
forge verify-contract --chain-id 43113 --rpc-url=${AVAX_RPC_URL} --watch ${AVAX_CONTRACT_ADDR} forge verify-contract --chain-id 43113 --rpc-url=${AVAX_RPC_URL} --watch ${AVAX_CONTRACT_ADDR}
@status: @status:
docker compose -f docker/docker-compose.yml ps docker compose -f docker/docker-compose.yml ps

View File

@ -11,6 +11,7 @@ Press F to pay respect
│ └── volumes.yml # Docker volume definitions │ └── volumes.yml # Docker volume definitions
├── src/ # Application source code ├── src/ # Application source code
│ ├── api/ # Backend logic (auth, user management) │ ├── api/ # Backend logic (auth, user management)
│ ├── contract/ # Smart contract files
│ ├── front/ # Frontend files │ ├── front/ # Frontend files
│ └── utils/ # Utility modules (auth, TOTP, etc.) │ └── utils/ # Utility modules (auth, TOTP, etc.)
├── flake.nix & flake.lock # Nix flake configuration ├── flake.nix & flake.lock # Nix flake configuration
@ -18,44 +19,44 @@ Press F to pay respect
``` ```
## Modules done ## Modules done
6 major + 2 minor = 7 full modules 8 major + 4 minor = 10 full modules
- **Web** - **Web**
- [x] Use a framework to build the backend.(node with Fastify) - Major - [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] Use a database for the backend -and more.(SQLite) - Minor
- [x] Store the score of a tournament in the Blockchain.(Soldity on Avalanche) - Major - [x] Store the score of a tournament in the Blockchain.(Soldity on Avalanche) - Major
- **User Management** - **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 - [x] Implement remote authentication. - Major
- **Gameplay and user experience** - **Gameplay and user experience**
- [ ] Remote players - Major - [ ] ~~Remote players - Major~~
- [ ] Multiplayer - Major - [ ] ~~Multiplayer - Major~~
- [ ] Add another game - Major - [x] Add another game - Major
- [ ] Game customization options - Minor - [ ] ~~Game customization options - Minor~~
- [ ] Live chat - Major - [ ] ~~Live chat - Major~~
- **AI-Algo** - **AI-Algo**
- [ ] AI opponent - Major - [ ] ~~AI opponent - Major~~
- [ ] User and game stats dashboards - Minor - [ ] User and game stats dashboards - Minor
- **Cybersecurity** - **Cybersecurity**
- [ ] WAF/ModSecurity and Hashicorp Vault - Major - [ ] ~~WAF/ModSecurity and Hashicorp Vault - Major~~
- [ ] RGPD compliance - Minor - [ ] ~~RGPD compliance - Minor~~
- [x] 2FA and JWT - Major - [x] 2FA and JWT - Major
- **DevOps** - **DevOps**
- [x] Infrasctructure setup for log management - Major - [x] Infrasctructure setup for log management - Major
- [x] Monitoring system - Minor - [x] Monitoring system - Minor
- [x] Designing the backend in micro-architecture - Major - [x] Designing the backend in micro-architecture - Major
- **Graphics** - **Graphics**
- [ ] Use of advanced 3D techniques - Major - [ ] ~~Use of advanced 3D techniques - Major~~
- **Accessibility** - **Accessibility**
- [ ] Support on all devices - Minor - [ ] ~~Support on all devices - Minor~~
- [ ] Expanding Browser compatibility - Minor - [x] Expanding Browser compatibility - Minor
- [ ] Multiple language support - Minor - [ ] ~~Multiple language support - Minor~~
- [ ] Add accessibility for visually impaired users - Minor - [ ] ~~Add accessibility for visually impaired users - Minor~~
- [ ] Server-Side Rendering (SSR) integration - Minor - [ ] ~~Server-Side Rendering (SSR) integration - Minor~~9
- **Server-Side Pong** - **Server-Side Pong**
- [ ] Replace basic pong with server-side pong and implementing an API - 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 - [ ] ~~Enabling pong gameplay via CLI against web users with API integration - Major~~
## License ## License

View File

@ -21,7 +21,8 @@ Input needed :
```json ```json
{ {
"user": "<string>", "user": "<string>",
"password": "<string>" "password": "<string>",
(optional)"token": "<2fa token>"
} }
``` ```
@ -32,7 +33,13 @@ Can return:
"msg": "Login successfully" "msg": "Login successfully"
} }
``` ```
- 400 with response - 402 with response
```json
{
"msg": "Please specify a 2fa token"
}
```
- 400 || 401 with response
```json ```json
{ {
"error": "<corresponding error>" "error": "<corresponding error>"

32
doc/auth/remove.md Normal file
View File

@ -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": "<corresponding msg>
}
```

120
doc/user/avatar.md Normal file
View File

@ -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
{
<FormData object containing the file>
}
```
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": "<corresponding error>"
}
```
- 404 with response (if the user does not exist)
```json
{
"error": "<corresponding error>"
}
```
## GET /users/:userId/avatar
Used to download an avatar
Input needed :
```json
{
<FormData object containing the file>
}
```
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": "<corresponding error>"
}
```
## PATCH /users/:userId/avatar
Used to modify an avatar
Input needed :
```json
{
<FormData object containing the file>
}
```
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": "<corresponding error>"
}
```
- 404 with response (if the user does not exist)
```json
{
"error": "<corresponding 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": "<corresponding error>"
}
```

155
doc/user/friend.md Normal file
View File

@ -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": "<corresponding error>"
}
```
- 404 with response (if user does not exist, or friend does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 401 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users/:userId/friends?iStart=<starting index (included)>&iEnd=<ending index (excluded)>`
Used to get the friends of an user
Can return:
- 200 with response (list of friend objects (between iStart and iEnd))
```json
{
"friends":
[
{
"friendName": "<the friend's username>",
"friendDisplayName": "<the friend's display name>"
},
...
]
}
```
- 400 with response (if iStart/iEnd is missing, or iEnd < iStart)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist, or no friends exist in the selected range)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users/:userId/friends/count`
Used to get the number of friends of an user
Can return:
- 200 with response
```json
{
"n_friends": <number of friends>
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding 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": "<corresponding error>"
}
```
- 401 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding 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": "<corresponding error>"
}
```
- 401 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist, or friend does not exist)
```json
{
"error": "<corresponding error>"
}
```

148
doc/user/matchHistory.md Normal file
View File

@ -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=<pong/tetris>`
Used to add a match result to an user to a specific game
Input needed :
```json
{
"game": "<pong/tetris>"
"opponent": "<the opponent's username>", <= item only present if the match involved 2 players
"myScore": <my score>,
"opponentScore": <the opponent's score>, <= item only present if the match involved 2 players
"date": <seconds since Epoch (Date.now() return)>
}
```
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": "<corresponding error>"
}
```
- 404 with response (if user does not exist, or opponent does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 401 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users/:userId/matchHistory?game=<pong/tetris>&iStart=<starting index (included)>&iEnd=<ending index (excluded)>`
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": "<the name of the p1>",
"p2": "<the name of the p2>", <= item only present if the match involved 2 players
"p1Score": "<the score of the p1>",
"p2Score": "<the score of the p2>", <= item only present if the match involved 2 players
"date": <seconds since Epoch (Date.now() return)>
},
"tx": "<the transcaction hash>"
},
...
]
}
```
- 400 with response (if iStart/iEnd does not exist, or iEnd < iStart, or the game specified is invalid)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist, or no matches exist in the selected range)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users/:userId/matchHistory/count?game=<pong/tetris>`
Used to get the number of matches an user played for a specific game
Can return:
- 200 with response
```json
{
"n_matches": <number of matches played by the user>
}
```
- 400 with response (if game does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```
## DELETE `/users/:userId/matchHistory?game=<pong/tetris>`
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": "<corresponding error>"
}
```
- 401 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```

41
doc/user/ping.md Normal file
View File

@ -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": "<true/false>"
}
```

212
doc/user/user.md Normal file
View File

@ -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": "<the display name>"
}
```
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": "<corresponding error>"
}
```
- 401 with response (if user specified in header is not admin)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users?iStart=<starting index (included)>&iEnd=<ending index (excluded)>`
Used to get the list of users
Can return:
- 200 with response (list of user objects (between iStart and iEnd))
```json
{
"users":
[
{
"username": "<the username>",
"displayName": "<the display name>",
"pong": {
"wins": <the number of pong matches won>,
"losses": <the number of pong matches lost>
},
"tetris": {
"wins": <the number of tetris matches won>,
"losses": <the number of tetris matches lost>
}
},
...
]
}
```
- 400 with response (if iStart/iEnd is missing, or iEnd < iStart)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if no users exist in the selected range)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users/count`
Used to get the number of users
Always returns:
- 200 with response
```json
{
"n_users": <number of users>
}
```
## GET `/users/:userId`
Used to get an user
Can return:
- 200 with response (an user object)
```json
{
"username": "<the username>",
"displayName": "<the display name>",
"pong": {
"wins": <the number of pong matches won>,
"losses": <the number of pong matches lost>
},
"tetris": {
"wins": <the number of tetris matches won>,
"losses": <the number of tetris matches lost>
}
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```
## PATCH `/users/:userId/:member`
Used to modify a member of an user (only displayName can be modified)
Input needed :
```json
{
"<the member to modify (must be identical to :member)>": "<the member's new value>"
}
```
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": "<corresponding error>"
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 401 with response (if user specified in header is not admin)
```json
{
"error": "<corresponding 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": "<corresponding 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": "<corresponding error>"
}
```
- 400 with response (if no user is specified in header, or member to delete does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```

View File

@ -14,6 +14,7 @@
"fastify-cli": "^7.4.0", "fastify-cli": "^7.4.0",
"pino": "^9.7.0", "pino": "^9.7.0",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"sharp": "^0.34.4",
"solhint": "^6.0.0" "solhint": "^6.0.0"
}, },
"type": "module", "type": "module",

264
pnpm-lock.yaml generated
View File

@ -50,6 +50,9 @@ importers:
prom-client: prom-client:
specifier: ^15.1.3 specifier: ^15.1.3
version: 15.1.3 version: 15.1.3
sharp:
specifier: ^0.34.4
version: 0.34.4
solhint: solhint:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0(typescript@5.8.3) version: 6.0.0(typescript@5.8.3)
@ -91,6 +94,9 @@ packages:
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@emnapi/runtime@1.5.0':
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
'@esbuild/aix-ppc64@0.25.6': '@esbuild/aix-ppc64@0.25.6':
resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -289,6 +295,132 @@ packages:
resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==}
engines: {node: '>=10.10.0'} 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': '@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@ -759,6 +891,10 @@ packages:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'} engines: {node: '>=8'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
dotenv-expand@10.0.0: dotenv-expand@10.0.0:
resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1458,6 +1594,10 @@ packages:
set-cookie-parser@2.7.1: set-cookie-parser@2.7.1:
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} 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: simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
@ -1673,6 +1813,11 @@ snapshots:
'@babel/helper-validator-identifier@7.27.1': {} '@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': '@esbuild/aix-ppc64@0.25.6':
optional: true optional: true
@ -1803,6 +1948,94 @@ snapshots:
'@humanwhocodes/momoa@2.0.4': {} '@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': '@isaacs/fs-minipass@4.0.1':
dependencies: dependencies:
minipass: 7.1.2 minipass: 7.1.2
@ -2194,6 +2427,8 @@ snapshots:
detect-libc@2.0.4: {} detect-libc@2.0.4: {}
detect-libc@2.1.2: {}
dotenv-expand@10.0.0: {} dotenv-expand@10.0.0: {}
dotenv@16.6.1: {} dotenv@16.6.1: {}
@ -2925,6 +3160,35 @@ snapshots:
set-cookie-parser@2.7.1: {} 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-concat@1.0.1: {}
simple-get@4.0.1: simple-get@4.0.1:

View File

@ -5,3 +5,4 @@ ignoredBuiltDependencies:
onlyBuiltDependencies: onlyBuiltDependencies:
- better-sqlite3 - better-sqlite3
- sharp

View File

@ -1,7 +1,8 @@
import fastifyJWT from '@fastify/jwt'; import fastifyJWT from '@fastify/jwt';
import fastifyCookie from '@fastify/cookie'; 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 { register } from './register.js';
import { login } from './login.js'; import { login } from './login.js';
import { gRedir } from './gRedir.js'; import { gRedir } from './gRedir.js';
@ -12,6 +13,7 @@ import { totpSetup } from './totpSetup.js';
import { totpDelete } from './totpDelete.js'; import { totpDelete } from './totpDelete.js';
import { totpVerify } from './totpVerify.js'; import { totpVerify } from './totpVerify.js';
import { logout } from './logout.js'; import { logout } from './logout.js';
import { remove } from './remove.js';
const saltRounds = 10; const saltRounds = 10;
export const appName = process.env.APP_NAME || 'knl_meowscendence'; export const appName = process.env.APP_NAME || 'knl_meowscendence';
@ -25,9 +27,9 @@ authDB.prepareDB();
export default async function(fastify, options) { export default async function(fastify, options) {
fastify.register(cors, { fastify.register(cors, {
origin: process.ENV.CORS_ORIGIN || 'http://localhost:5173', origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
credentials: true, credentials: true,
methods: [ "GET", "POST", "DELETE", "OPTIONS" ] methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"]
}); });
fastify.register(fastifyJWT, { fastify.register(fastifyJWT, {
@ -53,6 +55,9 @@ export default async function(fastify, options) {
fastify.get('/me', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.get('/me', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return { user: request.user }; return { user: request.user };
}); });
fastify.get('/2fa', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return totpCheck(request, reply);
});
// GOOGLE sign in // GOOGLE sign in
fastify.get('/login/google', async (request, reply) => { 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); }); }, async (request, reply) => { return register(request, reply, saltRounds, fastify); });
fastify.get('/logout', {}, async (request, reply) => { return logout(reply, fastify); }) fastify.get('/logout', {}, async (request, reply) => { return logout(reply, fastify); })
fastify.delete('/', { preHandler: fastify.authenticate }, async (request, reply) => { return remove(request, reply, fastify) })
} }

View File

@ -37,8 +37,8 @@ export async function login(request, reply, fastify) {
const userTOTP = authDB.getUser(user); const userTOTP = authDB.getUser(user);
if (userTOTP.totpEnabled == 1) { if (userTOTP.totpEnabled == 1) {
if (!request.body.token){ if (!request.body.token) {
return reply.code(401).send({ error: 'Invalid 2FA token' }); return reply.code(402).send({ error: 'Please specify a 2fa token' });
} }
const isValid = verifyTOTP(userTOTP.totpHash, request.body.token); const isValid = verifyTOTP(userTOTP.totpHash, request.body.token);
if (!isValid) { if (!isValid) {

35
src/api/auth/remove.js Normal file
View File

@ -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" });
}
}

24
src/api/auth/totpCheck.js Normal file
View File

@ -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" });
}
}

View File

@ -9,12 +9,11 @@ import { callAddScore, callLastId } from "../../utils/scoreStore_contract.js";
*/ */
export async function addTx(request, reply, fastify) { export async function addTx(request, reply, fastify) {
try { try {
const id = await callLastId(); const {tx, id} = await callAddScore(request.body.p1, request.body.p2, request.body.p1Score, request.body.p2Score);
const tx = 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({ return reply.code(200).send({
id: Number(id) id: Number(id)

View File

@ -20,7 +20,7 @@ export default async function(fastify, options) {
required: ['p1', 'p2', 'p1Score', 'p2Score'], required: ['p1', 'p2', 'p1Score', 'p2Score'],
properties: { properties: {
p1: { type: 'string', minLength: 1 }, p1: { type: 'string', minLength: 1 },
p2: { type: 'string', minLength: 1 }, p2: { type: 'string', minLength: 0 },
p1Score: { type: 'integer', minimum: 0 }, p1Score: { type: 'integer', minimum: 0 },
p2Score: { type: 'integer', minimum: 0 }, p2Score: { type: 'integer', minimum: 0 },
} }

24
src/api/user/dAvatar.js Normal file
View File

@ -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" });
}
}

23
src/api/user/dFriend.js Normal file
View File

@ -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" });
}
}

16
src/api/user/dFriends.js Normal file
View File

@ -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" });
}
}

View File

@ -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" });
}
}

22
src/api/user/dMember.js Normal file
View File

@ -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" });
}
}

19
src/api/user/dUser.js Normal file
View File

@ -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" });
}
}

View File

@ -2,66 +2,158 @@ import fastifyJWT from '@fastify/jwt';
import fastifyCookie from '@fastify/cookie'; import fastifyCookie from '@fastify/cookie';
import cors from '@fastify/cors' import cors from '@fastify/cors'
import Database from 'better-sqlite3'; 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; let database;
if (!env || env === 'development') { if (!env || env === 'development') {
database = new Database(":memory:", { verbose: console.log }); database = new Database(':memory:', { verbose: console.log });
} else { } else {
var dbPath = process.env.DB_PATH || '/db/db.sqlite' const dbPath = process.env.DB_PATH || '/db/db.sqlite'
database = new Database(dbPath); database = new Database(dbPath);
} }
function prepareDB() { function prepareDB() {
database.exec(` database.exec(`
CREATE TABLE IF NOT EXISTS userData ( CREATE TABLE IF NOT EXISTS userData (
username TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
displayName TEXT 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 ) STRICT
`); `);
database.exec(` database.exec(`
CREATE TABLE IF NOT EXISTS friends ( CREATE TABLE IF NOT EXISTS friends (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT, username TEXT,
friendName TEXT, friendName TEXT,
UNIQUE(username, friendName), UNIQUE(username, friendName),
CHECK(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(); prepareDB();
// POST // 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 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 // PATCH
const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;'); const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;');
const changeAvatarId = database.prepare('UPDATE userData SET avatarId = ? WHERE username = ?;');
// GET // GET
const getUserInfo = database.prepare('SELECT * FROM userData WHERE username = ?;'); const getUserData = database.prepare('SELECT username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses FROM userData LIMIT ? OFFSET ?;');
const getUserData = database.prepare('SELECT * FROM userData;'); 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 = ?;'); const getFriends = database.prepare('SELECT friendName FROM friends WHERE username = ? LIMIT ? OFFSET ?;');
// const isFriend = database.prepare('SELECT 1 FROM friends WHERE username = ? AND friendName = ?;'); 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 // DELETE
const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;'); const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;');
const deleteFriend = database.prepare('DELETE FROM friends WHERE username = ? AND friendName = ?;'); const deleteFriend = database.prepare('DELETE FROM friends WHERE username = ? AND friendName = ?;');
const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;'); 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').FastifyInstance} fastify
* @param {import('fastify').FastifyPluginOptions} options * @param {import('fastify').FastifyPluginOptions} options
*/ */
export default async function(fastify, options) { export default async function(fastify, options) {
fastify.register(cors, { fastify.register(cors, {
origin: process.ENV.CORS_ORIGIN || 'http://localhost:5173', origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
credentials: true, credentials: true,
methods: [ "GET", "POST", "DELETE", "OPTIONS" ] methods: [ "GET", "POST", "PATCH", "DELETE", "OPTIONS" ]
}); });
fastify.register(fastifyJWT, { fastify.register(fastifyJWT, {
@ -71,8 +163,19 @@ export default async function(fastify, options) {
}, },
}); });
fastify.register(fastifyCookie); 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 { try {
const jwt = await request.jwtVerify(); const jwt = await request.jwtVerify();
request.user = jwt.user; 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 { try {
const jwt = await request.jwtVerify(); const jwt = await request.jwtVerify();
if (jwt.user !== 'admin') { if (jwt.user !== 'admin') {
throw (""); throw ('You lack administrator privileges');
} }
request.user = jwt.user;
} catch (err) { } catch (err) {
reply.code(401).send({ error: 'Unauthorized' }); reply.code(401).send({ error: 'Unauthorized' });
} }
}); });
// GET // GET
fastify.get('/users', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.get('/users', { preHandler: [fastify.authenticate], schema: { querystring: querySchema } }, async (request, reply) => {
try { return gUsers(request, reply, fastify, getUserData);
const users = getUserData.all(); });
fastify.get('/users/count', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return reply.code(200).send({ users }); return gNumberUsers(request, reply, fastify, getNumberUsers);
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}); });
fastify.get('/users/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.get('/users/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => {
try { return gUser(request, reply, fastify, getUserInfo);
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" });
}
}); });
fastify.get('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.get('/users/:userId/friends', { preHandler: [fastify.authenticate], schema: { querystring: querySchema } }, async (request, reply) => {
try { return gFriends(request, reply, fastify, getUserInfo, getFriends);
const userId = request.params.userId; });
fastify.get('/users/:userId/friends/count', { preHandler: [fastify.authenticate] }, async (request, reply) => {
if (!getUserInfo.get(userId)) { return gNumberFriends(request, reply, fastify, getUserInfo, getNumberFriends);
return reply.code(404).send({ error: "User does not exist" }); });
} fastify.get('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { querystring: querySchemaMatchHistory } }, async (request, reply) => {
return gMatchHistory(request, reply, fastify, getUserInfo, getMatchHistory);
if (userId == request.user || request.user == 'admin') { });
const friends = getFriends.all(userId); fastify.get('/users/:userId/matchHistory/count', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => {
return gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches);
if (!friends) { });
return reply.code(404).send({ error: "User does not have friends D:" }); fastify.get('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => {
} return gAvatar(request, reply, fastify, getUserInfo, getAvatarId, getImage);
return reply.code(200).send({ friends }); });
} fastify.get('/ping/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => {
} catch (err) { return gPing(request, reply, fastify, getActivityTime);
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}); });
// POST // POST
fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => { fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => {
try { return pUser(request, reply, fastify, getUserInfo, createUser);
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" });
}
}); });
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 // PATCH
fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.patch('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => {
try { return uAvatar(request, reply, fastify, getUserInfo, setAvatarId, getAvatarId, deleteAvatarId, postImage, deleteImage);
const userId = request.params.userId; });
if (request.user != 'admin' && request.user != userId) { fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMember } }, async (request, reply) => {
return reply.code(401).send({ error: "Unauthorized" }); return uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId);
} });
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" });
}
})
// DELETE // DELETE
/**
* @description Can be used to delete a user from the db
*/
fastify.delete('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => { fastify.delete('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => {
try { return dUser(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteFriends, deleteUser);
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" });
}
}); });
fastify.delete('/users/:userId/:member', { preHandler: fastify.authenticate }, async (request, reply) => { fastify.delete('/users/:userId/:member', { preHandler: fastify.authenticate }, async (request, reply) => {
try { return dMember(request, reply, fastify, getUserInfo, changeDisplayName);
const user = request.user; });
const member = request.params.member; fastify.delete('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => {
if (user == 'admin' || user == request.params.userId) { return dFriends(request, reply, fastify, getUserInfo, deleteFriends);
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" });
}
}); });
fastify.delete('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { fastify.delete('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => {
try { return dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend);
const userId = request.params.userId; });
const friendId = request.params.friendId; fastify.delete('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => {
if (!getUserInfo.get(userId)) { return dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris);
return reply.code(404).send({ error: "User does not exist" }); });
} fastify.delete('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => {
if (request.user != 'admin' && request.user != userId) { return dAvatar(request, reply, fastify, getUserInfo, getAvatarId, deleteAvatarId, deleteImage);
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" });
}
}); });
} }

23
src/api/user/gAvatar.js Normal file
View File

@ -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" });
}
}

29
src/api/user/gFriends.js Normal file
View File

@ -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" });
}
}

View File

@ -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" });
}
}

View File

@ -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" });
}
}

View File

@ -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" });
}
}

View File

@ -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" });
}
}

28
src/api/user/gPing.js Normal file
View File

@ -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" });
}
}

24
src/api/user/gUser.js Normal file
View File

@ -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" });
}
}

28
src/api/user/gUsers.js Normal file
View File

@ -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" });
}
}

45
src/api/user/pAvatar.js Normal file
View File

@ -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" });
}
}

26
src/api/user/pFriend.js Normal file
View File

@ -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" });
}
}

View File

@ -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" });
}
}

21
src/api/user/pPing.js Normal file
View File

@ -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" });
}
}

19
src/api/user/pUser.js Normal file
View File

@ -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" });
}
}

48
src/api/user/uAvatar.js Normal file
View File

@ -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" });
}
}

23
src/api/user/uMember.js Normal file
View File

@ -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" });
}
}

View File

@ -11,7 +11,8 @@
<body class="bg-[url(https://y-syo.me/res/bg.jpg)] dark:bg-[url(https://api.kanel.ovh/random)] bg-center bg-cover h-screen flex flex-col font-[Lexend]">
<body class="bg-[url(https://api.kanel.ovh/random)] dark:bg-[url(https://api.kanel.ovh/random)] bg-center bg-cover h-screen flex flex-col font-[Lexend]">
<div class="absolute flex flex-col items-center space-y-5 top-4 left-5"> <div class="absolute flex flex-col items-center space-y-5 top-4 left-5">
<!--a class="absolute flex flex-col items-center top-4 left-5" href="/pong" data-icon--> <!--a class="absolute flex flex-col items-center top-4 left-5" href="/pong" data-icon-->

View File

@ -55,7 +55,47 @@ export default class extends Aview {
} }
async run() { async run() {
dragElement(document.getElementById("window")); dragElement(document.getElementById("window"));
const totpVerify = async () => {
const username = (document.getElementById("username") as HTMLInputElement).value;
const password = (document.getElementById("password") as HTMLInputElement).value;
const totpPin = (document.getElementById('totpPin') as HTMLInputElement).value;
const idWindow = (document.getElementById('2fa-popup-content') as HTMLInputElement);
try {
const data_req = await fetch("http://localhost:3001/login", {
method: "POST",
headers: { "Content-Type": "application/json", },
credentials: "include",
body: JSON.stringify({ user: username, password: password, token: totpPin }),
});
if (data_req.status === 200) {
isLogged();
navigationManager("/");
} else if (data_req.status === 401) {
const data = await data_req.json();
if (!document.getElementById("error-totp")) {
const error = document.createElement("p");
error.innerHTML = data.error;
error.classList.add("text-red-700", "dark:text-red-500");
idWindow.appendChild(error);
} else {
const error = document.getElementById("error-totp") as HTMLParagraphElement;
error.innerHTML = data.error;
}
} else {
console.log(data_req.status)
console.log(await data_req.json())
// throw new Error("invalid response");
}
} catch (error) {
console.error(error);
}
}
const login = async () => { const login = async () => {
const username = (document.getElementById("username") as HTMLInputElement).value; const username = (document.getElementById("username") as HTMLInputElement).value;
const password = (document.getElementById("password") as HTMLInputElement).value; const password = (document.getElementById("password") as HTMLInputElement).value;
@ -73,11 +113,69 @@ export default class extends Aview {
isLogged(); isLogged();
navigationManager("/"); navigationManager("/");
} }
else if (data_req.status === 402) {
const popup: HTMLDivElement = document.createElement("div");
popup.id = "2fa-popup";
popup.classList.add("z-10", "absolute", "default-border");
const header = popup.appendChild(document.createElement("div"));;
header.classList.add("bg-linear-to-r", "from-orange-200", "to-orange-300", "flex", "flex-row", "min-w-35", "justify-between", "px-2");
header.id = "2fa-header";
header.appendChild(document.createElement("span")).innerText = "2fa.ts";
const btn = header.appendChild(document.createElement("button"));
btn.innerText = " × ";
btn.onclick = () => { document.getElementById("2fa-popup").remove(); };
const popup_content: HTMLSpanElement = popup.appendChild(document.createElement("div"));
popup_content.id = "2fa-popup-content";
popup_content.classList.add("flex", "flex-col", "bg-neutral-200", "dark:bg-neutral-800", "p-6", "pt-4", "text-neutral-900", "dark:text-white", "space-y-4");
const tokenInput = document.createElement("input");
tokenInput.type = "tel";
tokenInput.id = "totpPin";
tokenInput.name = "totpPin";
tokenInput.placeholder = "TOTP code";
tokenInput.required = true;
tokenInput.autocomplete = "off";
tokenInput.pattern = "[0-9]*";
tokenInput.setAttribute("inputmode", "numeric");
tokenInput.classList.add("bg-white", "text-neutral-900","w-full", "px-4", "py-2", "input-border");
const tokenSubmit = document.createElement("button");
tokenSubmit.type = "submit";
tokenSubmit.classList.add("default-button", "w-full");
tokenSubmit.id = "totp-submit";
tokenSubmit.innerHTML = "submit";
const tokenTitle = document.createElement("h1");
tokenTitle.innerHTML = `hey ${username}, please submit your 2fa code below :`;
tokenTitle.classList.add("text-gray-900", "dark:text-white", "text-lg", "pt-0", "pb-4", "justify-center");
const form = document.createElement("form");
form.method = "dialog";
form.classList.add("space-y-4");
form.appendChild(tokenTitle);
form.appendChild(tokenInput);
form.appendChild(tokenSubmit);
popup_content.appendChild(form);
const uu = document.getElementById("username") as HTMLInputElement;
const pass = document.getElementById("password") as HTMLInputElement;
uu.disabled = true;
pass.disabled = true;
document.getElementById("app")?.appendChild(popup);
tokenInput.focus();
dragElement(document.getElementById("2fa-popup"));
document.getElementById("totp-submit")?.addEventListener("click", totpVerify);
}
else if (data_req.status === 400) else if (data_req.status === 400)
{ {
const data = await data_req.json(); const data = await data_req.json();
document.getElementById("login-error-message").innerHTML = "error: " + data.error; document.getElementById("login-error-message").innerHTML = "error: " + data.error;
document.getElementById("login-error-message").classList.remove("hidden"); document.getElementById("login-error-message").classList.remove("hidden");
} }
else else
{ {

View File

@ -138,6 +138,7 @@ export default class extends Aview {
if (!main) if (!main)
return console.error("what"); return console.error("what");
// don't read this shit for you mental health
if (matches.matchHistory) { if (matches.matchHistory) {
for (let match of matches.matchHistory) { for (let match of matches.matchHistory) {
if (match.score.p2 != undefined) if (match.score.p2 != undefined)
@ -202,7 +203,9 @@ export default class extends Aview {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
}); });
picture.src = a.status === 200 ? `http://localhost:3002/users/${uuid}/avatar` : "https://api.kanel.ovh/pp"; picture.src = a.status === 200
? `http://localhost:3002/users/${uuid}/avatar?t=${Date.now()}`
: "https://api.kanel.ovh/pp";
picture.classList.add("text-neutral-900", "dark:text-white", "center", "h-18", "w-18", "mx-3", "reverse-border"); picture.classList.add("text-neutral-900", "dark:text-white", "center", "h-18", "w-18", "mx-3", "reverse-border");
const nametag = profile.appendChild(document.createElement("div")); const nametag = profile.appendChild(document.createElement("div"));

View File

@ -40,13 +40,13 @@ export default class extends Aview {
async function getMainHTML() { async function getMainHTML() {
if (!(await isLogged())) if (!(await isLogged()))
{ {
document.getElementById("menu-bottom-div").classList.add("hidden"); document.getElementById("menu-bottom-div")?.classList.add("hidden");
return ` return `
<a class="menu-default-button inline-flex items-center justify-center" href="/login" data-link>login</a> <a class="menu-default-button inline-flex items-center justify-center" href="/login" data-link>login</a>
<a class="menu-default-button inline-flex items-center justify-center" href="/register" data-link>register</a> <a class="menu-default-button inline-flex items-center justify-center" href="/register" data-link>register</a>
`; `;
} }
document.getElementById("menu-bottom-div").classList.remove("hidden"); document.getElementById("menu-bottom-div")?.classList.remove("hidden");
uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, { const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, {
@ -82,6 +82,6 @@ export default class extends Aview {
else else
console.error("logout failed"); console.error("logout failed");
}); });
}); document.getElementById("profile-items").innerHTML = await getMainHTML();
} }
} }

View File

@ -5,8 +5,7 @@ import { isLogged, navigationManager } from "../main.ts"
export default class extends Aview { export default class extends Aview {
constructor() constructor() {
{
super(); super();
this.setTitle("register"); this.setTitle("register");
setOnekoState("default"); setOnekoState("default");
@ -55,7 +54,7 @@ export default class extends Aview {
} }
async run() { async run() {
dragElement(document.getElementById("window")); dragElement(document.getElementById("window"));
const login = async () => { const login = async () => {
const username = (document.getElementById("username") as HTMLInputElement).value; const username = (document.getElementById("username") as HTMLInputElement).value;
const password = (document.getElementById("password") as HTMLInputElement).value; const password = (document.getElementById("password") as HTMLInputElement).value;
@ -69,8 +68,7 @@ export default class extends Aview {
}); });
const data = await data_req.json(); const data = await data_req.json();
if (data_req.status === 200) if (data_req.status === 200) {
{
let uuid_req = await fetch("http://localhost:3001/me", { let uuid_req = await fetch("http://localhost:3001/me", {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
@ -80,22 +78,23 @@ export default class extends Aview {
isLogged(); isLogged();
navigationManager("/"); navigationManager("/");
} }
else if (data_req.status === 400) else if (data_req.status === 400) {
{ if (document.getElementById("login-error-message")) {
document.getElementById("login-error-message").innerHTML = "error: " + data.error; document.getElementById("login-error-message").innerHTML = "error: " + data.error;
document.getElementById("login-error-message").classList.remove("hidden"); document.getElementById("login-error-message")?.classList.remove("hidden");
}
} }
else else {
{
throw new Error("invalid response"); throw new Error("invalid response");
} }
} }
catch (error) catch (error) {
{
console.error(error); console.error(error);
document.getElementById("login-error-message").innerHTML = "error: server error, try again later..."; if (document.getElementById("login-error-message")) {
document.getElementById("login-error-message").classList.remove("hidden"); document.getElementById("login-error-message").innerHTML = "error: server error, try again later...";
document.getElementById("login-error-message")?.classList.remove("hidden");
}
} }
}; };

View File

@ -2,12 +2,12 @@ import Aview from "./Aview.ts"
import { dragElement } from "./drag.ts"; import { dragElement } from "./drag.ts";
import { setOnekoState } from "../oneko.ts" import { setOnekoState } from "../oneko.ts"
import { isLogged, navigationManager } from "../main.ts" import { isLogged, navigationManager } from "../main.ts"
import { totpEnablePopup } from "./TotpEnable.ts";
import { totpVerify } from "../../../../api/auth/totpVerify.js";
export default class extends Aview { export default class extends Aview {
constructor() constructor() {
{
super(); super();
this.setTitle("profile"); this.setTitle("profile");
setOnekoState("default"); setOnekoState("default");
@ -24,7 +24,7 @@ export default class extends Aview {
<a href="/" data-link> × </a> <a href="/" data-link> × </a>
</div> </div>
</div> </div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-8 reverse-border"> <div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<div class="flex flex-row items-center place-items-center space-x-4"> <div class="flex flex-row items-center place-items-center space-x-4">
<input type="text" id="displayName-input" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input> <input type="text" id="displayName-input" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<button id="displayName-button" type="submit" class="default-button w-full">change display name</button> <button id="displayName-button" type="submit" class="default-button w-full">change display name</button>
@ -37,6 +37,10 @@ export default class extends Aview {
<label for="upload-file" class="default-button">select an avatar...</label><input type="file" id="upload-file" class="hidden" accept="image/*" /> <label for="upload-file" class="default-button">select an avatar...</label><input type="file" id="upload-file" class="hidden" accept="image/*" />
</div> </div>
<button id="deleteAccount-button" type="submit" class="default-button w-full">delete your account</button> <button id="deleteAccount-button" type="submit" class="default-button w-full">delete your account</button>
<div class="flex justify-center">
<hr class="w-50 reverse-border">
</div>
<button id="2fa-button" type="submit" class="default-button w-full">2fa</button>
</div> </div>
</div> </div>
`; `;
@ -48,6 +52,21 @@ export default class extends Aview {
dragElement(document.getElementById("window")); dragElement(document.getElementById("window"));
const isTOTPEnabled = async () => {
const totpVerify_req = await fetch('http://localhost:3001/2fa', {
method: "GET",
credentials: "include"
})
if (totpVerify_req.status === 200) {
const totpVerify_data = await totpVerify_req.json();
if (totpVerify_data.totp == true) {
return true;
}
}
return false;
};
let uuid: String; let uuid: String;
uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, { const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, {
@ -123,5 +142,40 @@ export default class extends Aview {
}); });
console.log(up_req.status); console.log(up_req.status);
}; };
const totpButton = document.getElementById("2fa-button") as HTMLButtonElement;
if ((await isTOTPEnabled()) === true) {
totpButton.innerHTML = "disable 2fa";
document.getElementById("2fa-button")?.addEventListener("click", async () => {
const totp_req = await fetch(`http://localhost:3001/2fa`, {
method: "DELETE",
credentials: "include"
})
if (totp_req.status === 200) {
console.log("working")
navigationManager("/settings")
} else {
console.log("wut")
}
});
} else {
totpButton.innerHTML = "enable 2fa";
document.getElementById("2fa-button")?.addEventListener("click", async () => {
const totp_req = await fetch(`http://localhost:3001/2fa`, {
method: "POST",
credentials: "include"
})
if (totp_req.status === 200) {
console.log("working")
const totp_data = await totp_req.json();
totpEnablePopup(uuid, totp_data.secret, totp_data.otpauthUrl);
} else {
console.log("wut")
}
});
}
} }
} }

View File

@ -0,0 +1,110 @@
import { navigationManager } from "../main.ts";
import { dragElement } from "./drag.ts";
async function totpVerify() {
const code = (document.getElementById("totpPin") as HTMLInputElement).value;
const data_req = await fetch('http://localhost:3001/2fa/verify', {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
token: code
})
})
if (data_req.status === 200) {
navigationManager("/settings");
} else if (data_req.status === 401 || data_req.status === 400) {
const popup_content = document.getElementById("2fa-enable-content");
if (!document.getElementById("error-totp")) {
const error = document.createElement("p");
error.id = "error-totp";
error.classList.add("text-red-700", "dark:text-red-500", "text-center");
error.innerHTML = (await data_req.json()).error;
popup_content?.appendChild(error)
} else {
const error = document.getElementById("error-totp") as HTMLParagraphElement;
error.innerHTML = (await data_req.json()).error;
}
} else {
console.log("Unexpected error")
}
}
export async function totpEnablePopup(username: String, secret: String, url: String) {
const popup: HTMLDivElement = document.createElement("div");
popup.id = "2fa-enable-popup";
popup.classList.add("z-10", "absolute", "default-border");
const header = popup.appendChild(document.createElement("div"));;
header.classList.add("bg-linear-to-r", "from-orange-200", "to-orange-300", "flex", "flex-row", "min-w-35", "justify-between", "px-2");
header.id = "2fa-enable-popup-header";
header.appendChild(document.createElement("span")).innerText = "2fa_enable.ts";
const btn = header.appendChild(document.createElement("button"));
btn.innerText = " × ";
btn.onclick = () => { document.getElementById("2fa-enable-popup")?.remove(); };
const popup_content: HTMLSpanElement = popup.appendChild(document.createElement("div"));
popup_content.id = "2fa-enable-content";
popup_content.classList.add("flex", "flex-col", "bg-neutral-200", "dark:bg-neutral-800", "p-6", "pt-4", "text-neutral-900", "dark:text-white", "space-y-4");
const qrDivTOTP = document.createElement("div");
qrDivTOTP.classList.add("flex", "justify-center");
const qrCodeTOTP = document.createElement("img");
qrCodeTOTP.id = "qrCodeTOTP";
qrCodeTOTP.src = `https://api.qrserver.com/v1/create-qr-code/?margin=10&size=512x512&data=${url}`;
qrCodeTOTP.classList.add("w-60");
qrDivTOTP.appendChild(qrCodeTOTP);
const secretText = document.createElement("p");
secretText.innerHTML = `key: <div class="select-all">${secret}</div>`;
secretText.classList.add("text-center")
const tokenInput = document.createElement("input");
tokenInput.type = "tel";
tokenInput.id = "totpPin";
tokenInput.name = "totpPin";
tokenInput.placeholder = "TOTP code";
tokenInput.required = true;
tokenInput.autocomplete = "off";
tokenInput.pattern = "[0-9]*";
tokenInput.setAttribute("inputmode", "numeric");
tokenInput.classList.add("bg-white", "text-neutral-900", "w-full", "px-4", "py-2", "input-border");
const tokenSubmit = document.createElement("button");
tokenSubmit.type = "submit";
tokenSubmit.classList.add("default-button", "w-full");
tokenSubmit.id = "totp-submit";
tokenSubmit.innerHTML = "submit";
const hr = document.createElement("hr");
hr.classList.add("my-2", "w-full", "reverse-border");
const t = document.createElement("h2");
t.innerHTML = "hey " + username +
` you are trying to add 2fa</br>
just add the following to your app and enter the code bellow ↓
`;
t.classList.add("text-center")
document.getElementById("app")?.appendChild(popup);
const form = document.createElement("form");
form.method = "dialog";
form.classList.add("space-y-4");
form.appendChild(tokenInput);
form.appendChild(tokenSubmit);
popup_content.appendChild(t)
popup_content.appendChild(qrDivTOTP);
popup_content.appendChild(secretText);
popup_content.appendChild(hr)
popup_content.appendChild(form);
dragElement(document.getElementById("2fa-enable-popup"));
document.getElementById("totp-submit")?.addEventListener("click", totpVerify)
}

View File

@ -82,9 +82,15 @@ function getUser(user) {
return stmt.get(user); return stmt.get(user);
} }
function rmUser(user) {
const stmt = database.prepare('DELETE FROM credentials WHERE username = ?');
stmt.run(user);
}
const authDB = { const authDB = {
prepareDB, prepareDB,
checkUser, checkUser,
rmUser,
addUser, addUser,
passwordQuery, passwordQuery,
setTOTPSecret, setTOTPSecret,

View File

@ -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,
},
}
);
}

View File

@ -29,7 +29,7 @@ async function loadContract() {
async function callGetScore(id) { async function callGetScore(id) {
try { try {
const contract = await loadContract(); const contract = await loadContract();
const result = await contract.getScore(id); const result = await contract.getScore(id - 1);
return result; return result;
} catch (error) { } catch (error) {
console.error('Error calling view function:', 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); const tx = await contract.addScore(p1, p2, p1Score, p2Score);
console.log('Transaction sent:', tx.hash); console.log('Transaction sent:', tx.hash);
await tx.wait(); // Wait for the transaction to be mined await tx.wait(); // Wait for the transaction to be mined
const id = await callLastId();
console.log('Transaction confirmed'); console.log('Transaction confirmed');
return tx; return { tx, id };
} catch (error) { } catch (error) {
console.error('Error calling addScore function:', error); console.error('Error calling addScore function:', error);
throw error; throw error;