diff --git a/.env.example b/.env.example index 34d30e2..f89d5a2 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,26 @@ GRAPH_PORT=3000 ELK_PORT=5601 -GOOGLE_CALLBACK_URL=https://localhost:8443/api/v1 +# the url to which the user will be redirected when it logs with google +CALLBACK_REDIR=http://localhost:3000 GOOGLE_CLIENT_SECRET=susAF GOOGLE_CLIENT_ID=Really +AVAX_PRIVATE_KEY= +AVAX_RPC_URL= +AVAX_CONTRACT_ADDR= + +SMTP_SMARTHOST= +SMTP_FROM= +SMTP_AUTH_USERNAME= +SMTP_AUTH_PASSWORD= +EMAIL_TO= + +# all of those can't have a / at the env ↓ +USER_URL= +AUTH_URL= +SCORE_URL= + +CORS_ORIGIN= +VITE_USER_URL= +VITE_AUTH_URL= diff --git a/Justfile b/Justfile index a9024ab..442d718 100644 --- a/Justfile +++ b/Justfile @@ -42,17 +42,19 @@ set dotenv-load # To completely docker @clean-docker: clean-compose - docker system prune -af + docker builder prune -f + docker volume prune -af + # docker system prune -af # To clean only the container launched by the compose @clean-compose: stop-docker docker compose -f docker/docker-compose.yml rm @deploy-contract-scoreStore: - forge create scoreStore --rpc-url=${RPC_URL} --private-key=${PRIVATE_KEY} + forge create scoreStore --rpc-url=${AVAX_RPC_URL} --private-key=${AVAX_PRIVATE_KEY} --broadcast @verify-contract: forge verify-contract --chain-id 43113 --rpc-url=${AVAX_RPC_URL} --watch ${AVAX_CONTRACT_ADDR} @status: - docker compose -f docker/docker-compose.yml ps \ No newline at end of file + docker compose -f docker/docker-compose.yml ps diff --git a/LICENSE b/LICENSE index 318cf61..da81945 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 adjoly, ttrave, mmoussou +Copyright (c) 2025 adjoly, ttrave, nmoussou Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9d75505..e9c72c0 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Press F to pay respect │ └── volumes.yml # Docker volume definitions ├── src/ # Application source code │ ├── api/ # Backend logic (auth, user management) +│ ├── contract/ # Smart contract files │ ├── front/ # Frontend files │ └── utils/ # Utility modules (auth, TOTP, etc.) ├── flake.nix & flake.lock # Nix flake configuration @@ -18,44 +19,44 @@ Press F to pay respect ``` ## Modules done -6 major + 2 minor = 7 full modules +8 major + 4 minor = 10 full modules - **Web** - [x] Use a framework to build the backend.(node with Fastify) - Major - - [ ] Use a framework or toolkit to build the front-end.(Tailwind CSS) - Minor + - [x] Use a framework or toolkit to build the front-end.(Tailwind CSS) - Minor - [x] Use a database for the backend -and more.(SQLite) - Minor - [x] Store the score of a tournament in the Blockchain.(Soldity on Avalanche) - Major - **User Management** - - [ ] Standard user management, authentication and users across tournaments. - Major + - [x] Standard user management, authentication and users across tournaments. - Major - [x] Implement remote authentication. - Major - **Gameplay and user experience** - - [ ] Remote players - Major - - [ ] Multiplayer - Major - - [ ] Add another game - Major - - [ ] Game customization options - Minor - - [ ] Live chat - Major + - [ ] ~~Remote players - Major~~ + - [ ] ~~Multiplayer - Major~~ + - [x] Add another game - Major + - [ ] ~~Game customization options - Minor~~ + - [ ] ~~Live chat - Major~~ - **AI-Algo** - - [ ] AI opponent - Major - - [ ] User and game stats dashboards - Minor + - [ ] ~~AI opponent - Major~~ + - [ ] ~~User and game stats dashboards - Minor~~ - **Cybersecurity** - - [ ] WAF/ModSecurity and Hashicorp Vault - Major - - [ ] RGPD compliance - Minor + - [ ] ~~WAF/ModSecurity and Hashicorp Vault - Major~~ + - [ ] ~~RGPD compliance - Minor~~ - [x] 2FA and JWT - Major - **DevOps** - [x] Infrasctructure setup for log management - Major - [x] Monitoring system - Minor - [x] Designing the backend in micro-architecture - Major - **Graphics** - - [ ] Use of advanced 3D techniques - Major + - [ ] ~~Use of advanced 3D techniques - Major~~ - **Accessibility** - - [ ] Support on all devices - Minor - - [ ] Expanding Browser compatibility - Minor - - [ ] Multiple language support - Minor - - [ ] Add accessibility for visually impaired users - Minor - - [ ] Server-Side Rendering (SSR) integration - Minor + - [ ] ~~Support on all devices - Minor~~ + - [x] Expanding Browser compatibility - Minor + - [ ] ~~Multiple language support - Minor~~ + - [ ] ~~Add accessibility for visually impaired users - Minor~~ + - [ ] ~~Server-Side Rendering (SSR) integration - Minor~~9 - **Server-Side Pong** - - [ ] Replace basic pong with server-side pong and implementing an API - Major - - [ ] Enabling pong gameplay via CLI against web users with API integration - Major + - [ ] ~~Replace basic pong with server-side pong and implementing an API - Major~~ + - [ ] ~~Enabling pong gameplay via CLI against web users with API integration - Major~~ ## License diff --git a/doc/auth/2fa.md b/doc/auth/2fa.md new file mode 100644 index 0000000..ef087b4 --- /dev/null +++ b/doc/auth/2fa.md @@ -0,0 +1,68 @@ +# 2fa + +Abailable endpoints: +- POST `/2fa` +- POST `/2fa/verify` +- DELETE `/2fa` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/2fa` + +Used to enable 2fa (need to verify after to confirm) + +Inputs: just need a valid JWT cookie + +Returns: +- 200 +```json +{ + "secret": "" + "otpauthUrl": "" +} +``` + +## POST `/2fa/verify` + +Used to confirm 2fa + +Inputs: a valid JWT in cookie and +```json +{ + "token": "" +} +``` + +Returns: +- 200 +```json +{ + "msg": "2FA verified successfully" +} +``` +- 401 || 400 || 404 +```json +{ + "error": "" +} +``` + +## DELETE `/2fa` + +Used to remove 2fa + +Inputs: a valid JWT in cookie + +Returns: +- 200 +```json +{ + "msg": "TOTP removed" +} +``` diff --git a/doc/auth/login.md b/doc/auth/login.md new file mode 100644 index 0000000..eff517d --- /dev/null +++ b/doc/auth/login.md @@ -0,0 +1,72 @@ +# Login + +Abailable endpoints: +- POST `/login` +- GET `/login/google` +- GET `/login/google/callback` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/login` + +Used to login + +Input needed : +```json +{ + "user": "", + "password": "", + (optional)"token": "<2fa token>" +} +``` + +Can return: +- 200 with response and cookie in header +```json +{ + "msg": "Login successfully" +} +``` +- 402 with response +```json +{ + "msg": "Please specify a 2fa token" +} +``` +- 400 || 401 with response +```json +{ + "error": "" +} +``` + +## GET `/login/google` + +Used to redirect the user to the login page for google auth + +Always return: +- redirect to the google auth url + +## GET `/login/google/callback` + +Used to get the callback from google and confirm the login + +Can return: +- 400 with response +```json +{ + "error": "" +} +``` +- 200 with response and cookie in header +```json +{ + "msg": "Login successfully" +} +``` diff --git a/doc/auth/logout.md b/doc/auth/logout.md new file mode 100644 index 0000000..98b6edc --- /dev/null +++ b/doc/auth/logout.md @@ -0,0 +1,24 @@ +# Logout + +Available endpoints: +- GET `/logout` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## GET `/logout` + +Used to logout the client (it just delete the cookie) + +Returns: +- 200 with response and clear cookie +```json +{ + "msg": "Logout successful" +} +``` diff --git a/doc/auth/me.md b/doc/auth/me.md new file mode 100644 index 0000000..26a2e15 --- /dev/null +++ b/doc/auth/me.md @@ -0,0 +1,11 @@ +GET `/me` + +Inputs : just need the JWT cookie + +Returns the user of the account + +``` +{ + user: ":userId" +} +``` diff --git a/doc/auth/register.md b/doc/auth/register.md new file mode 100644 index 0000000..55caca5 --- /dev/null +++ b/doc/auth/register.md @@ -0,0 +1,65 @@ +# Register + +Available endpoints: +- POST `/register` +- GET `/register/google` +- GET `/register/google/callback` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/register` + +Used to register + +Input needed : +```json +{ + "user": "", + "password": "" +} +``` + +Can return: +- 200 with response and cookie in header +```json +{ + "msg": "Register successfully" +} +``` +- 400 with response +```json +{ + "error": "" +} +``` + +## GET `/register/google` + +Used to redirect to the google auth page + +Always return: +- redirect to the google auth url + +## GET `/register/google/callback` + +Used to get the callback from google and register + +Can return: +- 400 with response +```json +{ + "error": "" +} +``` +- 200 with response and cookie in header +```json +{ + "msg": "Register successfully" +} +``` diff --git a/doc/auth/remove.md b/doc/auth/remove.md new file mode 100644 index 0000000..366c89c --- /dev/null +++ b/doc/auth/remove.md @@ -0,0 +1,32 @@ +# remove user + +Available endpoints: +- DELETE `/` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## DELETE `/` + +User to remove a user from the backend + +Inputs: just need a valid JWT cookie + +Returns: +- 200 +```json +{ + "msg": "User successfully deleted" +} +``` +- 401 || 400 +```json +{ + "error": " +} +``` diff --git a/doc/scoreStore/README.md b/doc/scoreStore/README.md new file mode 100644 index 0000000..0276a44 --- /dev/null +++ b/doc/scoreStore/README.md @@ -0,0 +1,56 @@ +# scoreStore + +Available endpoints: +- GET `/:id` +- POST `/` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## GET `/:id` + +Used to get an score from the blockchain (the id is the one returned when a score is added) + +Inputs: +:id : the id of the score + +Returns: +- 200 +```json +{ + "score": { + "p1": "", + "p2": "", + "p1Score": "", + "p2Score": "" + }, + "tx": "" +} +``` + +## POST `/` + +Used to add a new score (note that those can't be removed after added) + +Inputs (this one need to be the same as the following otherwise you will have an error 500): +```json +{ + "p1": "", + "p2": "", + "p1Score": "", + "p2Score": "" +} +``` + +Returns: +- 200 +```json +{ + "id": "" +} +``` diff --git a/doc/user/avatar.md b/doc/user/avatar.md new file mode 100644 index 0000000..d3232e4 --- /dev/null +++ b/doc/user/avatar.md @@ -0,0 +1,120 @@ +# Avatar + +Available endpoints: +- POST `/users/:userId/avatar` +- GET `/users/:userId/avatar` +- PATCH `/users/:userId/avatar` +- DELETE `/users/:userId/avatar` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST /users/:userId/avatar + +Used to upload an avatar + +Input needed : +```json +{ + +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Avatar uploaded successfully" +} +``` +- 400 with response (if the file is too large, or file is missing, or it is missing a file name, or it is missing a mime type) +```json +{ + "error": "" +} +``` +- 404 with response (if the user does not exist) +```json +{ + "error": "" +} +``` + +## GET /users/:userId/avatar + +Used to download an avatar + +Input needed : +```json +{ + +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Avatar uploaded successfully" +} +``` +- 404 with response (if the user does not exist, or the user does not have an assigned avatar, or the image does not exist) +```json +{ + "error": "" +} +``` + +## PATCH /users/:userId/avatar + +Used to modify an avatar + +Input needed : +```json +{ + +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Avatar modified successfully" +} +``` +- 400 with response (if the file is too large, or file is missing, or it is missing a file name, or it is missing a mime type) +```json +{ + "error": "" +} +``` +- 404 with response (if the user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE /users/:userId/avatar + +Used to delete an avatar + +Can return: +- 200 with response +```json +{ + "msg": "Avatar deleted successfully" +} +``` +- 404 with response (if the user does not exist, or the user does not have an assigned avatar) +```json +{ + "error": "" +} +``` diff --git a/doc/user/friend.md b/doc/user/friend.md new file mode 100644 index 0000000..377af5c --- /dev/null +++ b/doc/user/friend.md @@ -0,0 +1,155 @@ +# Friend + +Available endpoints: +- POST `/users/:userId/friends` +- GET `/users/:userId/friends` +- GET `/users/:userId/friends/count` +- DELETE `/users/:userId/friends` +- DELETE `/users/:userId/friends/:friendId` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/users/:userId/friends/:friendId` + +Used to add a friend to an user + +Can return: +- 200 with response +```json +{ + "msg": "Friend added successfully" +} +``` +- 400 with response (if no user is specified in header, or friend is the user specified in header, or friend is already added) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or friend does not exist) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/friends?iStart=&iEnd=` + +Used to get the friends of an user + +Can return: +- 200 with response (list of friend objects (between iStart and iEnd)) +```json +{ + "friends": + [ + { + "friendName": "", + "friendDisplayName": "" + }, + ... + ] +} +``` +- 400 with response (if iStart/iEnd is missing, or iEnd < iStart) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or no friends exist in the selected range) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/friends/count` + +Used to get the number of friends of an user + +Can return: +- 200 with response +```json +{ + "n_friends": +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/friends` + +Used to delete the friends of an user + +Can return: +- 200 with response +```json +{ + "msg": "Friends deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/friends/:friendId` + +Used to delete a friend of an user + +Can return: +- 200 with response +```json +{ + "msg": "Friend deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or friend does not exist) +```json +{ + "error": "" +} +``` diff --git a/doc/user/matchHistory.md b/doc/user/matchHistory.md new file mode 100644 index 0000000..6145a8d --- /dev/null +++ b/doc/user/matchHistory.md @@ -0,0 +1,148 @@ +# Match History + +Available endpoints: +- POST `/users/:userId/matchHistory` +- GET `/users/:userId/matchHistory` +- GET `/users/:userId/matchHistory/count` +- DELETE `/users/:userId/matchHistory` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/users/:userId/matchHistory?game=` + +Used to add a match result to an user to a specific game + +Input needed : +```json +{ + "game": "" + "opponent": "", <= item only present if the match involved 2 players + "myScore": , + "opponentScore": , <= item only present if the match involved 2 players + "date": +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Match successfully saved to the blockchain" +} +``` +- 400 with response (if no user is specified in header, or no opponent/p1Score/p2Score is specified in body, or opponent is the user specified in header, or a score is negative, or the game specified is invalid, or the game should involve more players than was specified) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or opponent does not exist) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/matchHistory?game=&iStart=&iEnd=` + +Used to get the match history of an user for a specific game + +Can return: +- 200 with response (list of matches results (between iStart and iEnd)) +```json +{ + "matchHistory": + [ + { + "score": + { + "p1": "", + "p2": "", <= item only present if the match involved 2 players + "p1Score": "", + "p2Score": "", <= item only present if the match involved 2 players + "date": + }, + "tx": "" + }, + ... + ] +} +``` +- 400 with response (if iStart/iEnd does not exist, or iEnd < iStart, or the game specified is invalid) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or no matches exist in the selected range) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/matchHistory/count?game=` + +Used to get the number of matches an user played for a specific game + +Can return: +- 200 with response +```json +{ + "n_matches": +} +``` +- 400 with response (if game does not exist) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/matchHistory?game=` + +Used to delete the match history of an user for a specific game + +Can return: +- 200 with response +```json +{ + "msg": "Match history deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user, or the game specified is invalid) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` diff --git a/doc/user/ping.md b/doc/user/ping.md new file mode 100644 index 0000000..1e6dded --- /dev/null +++ b/doc/user/ping.md @@ -0,0 +1,41 @@ +# ping + +Available endpoints: +- POST `/ping` +- GET `/ping/:userId` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/ping` + +Used to send a ping and update the lastSeenTime (can be used for activity time) + +Input needed : just need a valid token + +Can return: +- 200 +```json +{ + "msg": "last seen time updated successfully" +} +``` + +## GET `/ping/:userId` + +Used to retrive the lastSeenTime of a user + +Input needed : just need a valid token + +Can return: +- 200 +```json +{ + "isLogged": "" +} +``` diff --git a/doc/user/user.md b/doc/user/user.md new file mode 100644 index 0000000..1f57d58 --- /dev/null +++ b/doc/user/user.md @@ -0,0 +1,212 @@ +# User + +Available endpoints: +- POST `/users/:userId` +- GET `/users` +- GET `/users/count` +- GET `/users/:userId` +- PATCH `/users/:userId/:member` +- DELETE `/users/:userId` +- DELETE `/users/:userId/:member` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/users/:userId` + +Used to create an user + +Input needed : +```json +{ + "displayName": "" +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "User created successfully" +} +``` +- 400 with response (if no user is specified in header, or user already exists, or no display name is specified in body) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is not admin) +```json +{ + "error": "" +} +``` + +## GET `/users?iStart=&iEnd=` + +Used to get the list of users + +Can return: +- 200 with response (list of user objects (between iStart and iEnd)) +```json +{ + "users": + [ + { + "username": "", + "displayName": "", + "pong": { + "wins": , + "losses": + }, + "tetris": { + "wins": , + "losses": + } + }, + ... + ] +} +``` +- 400 with response (if iStart/iEnd is missing, or iEnd < iStart) +```json +{ + "error": "" +} +``` +- 404 with response (if no users exist in the selected range) +```json +{ + "error": "" +} +``` + +## GET `/users/count` + +Used to get the number of users + +Always returns: +- 200 with response +```json +{ + "n_users": +} +``` + +## GET `/users/:userId` + +Used to get an user + +Can return: +- 200 with response (an user object) +```json +{ + "username": "", + "displayName": "", + "pong": { + "wins": , + "losses": + }, + "tetris": { + "wins": , + "losses": + } +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## PATCH `/users/:userId/:member` + +Used to modify a member of an user (only displayName can be modified) + +Input needed : +```json +{ + "": "" +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "<:member> modified sucessfully" +} +``` +- 400 with response (if no user is specified in header, or new value of member to modify is not provided in the body, or member does not exist) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is not admin) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId` + +Used to delete an user + +Can return: +- 200 with response +```json +{ + "msg": "User deleted successfully" +} +``` +- 404 with response (user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/:member` + +Used to delete a member of an user (only displayName can be deleted) + +Can return: +- 200 with response +```json +{ + "msg": "<:member> deleted successfully" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 400 with response (if no user is specified in header, or member to delete does not exist) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` diff --git a/docker/ELK/logstash/compose.yml b/docker/ELK/logstash/compose.yml index be01856..9bc1dd4 100644 --- a/docker/ELK/logstash/compose.yml +++ b/docker/ELK/logstash/compose.yml @@ -8,6 +8,7 @@ services: - log-user:/var/log/user-api - log-auth:/var/log/auth-api - log-nginx:/var/log/nginx + - log-scoreStore:/var/log/scoreStore environment: - LOG_LEVEL=info networks: diff --git a/docker/ELK/logstash/pipeline/logstash.conf b/docker/ELK/logstash/pipeline/logstash.conf index 1a53ae5..aedf06c 100644 --- a/docker/ELK/logstash/pipeline/logstash.conf +++ b/docker/ELK/logstash/pipeline/logstash.conf @@ -19,6 +19,11 @@ input { start_position => "beginning" tags => [ "nginx", "front", "error" ] } + file { + path => "/var/log/scoreStore/log.log" + start_position => "beginning" + tags => [ "api", "scoreStore" ] + } } output { diff --git a/docker/api-base/Dockerfile b/docker/api-base/Dockerfile index 142d056..e291122 100644 --- a/docker/api-base/Dockerfile +++ b/docker/api-base/Dockerfile @@ -6,7 +6,7 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml /app/ # install all the dependency RUN npm install -g pnpm RUN cd /app \ - && pnpm install --prod + && pnpm install --prod --frozen-lockfile FROM node:lts-alpine AS base diff --git a/docker/api-base/compose.yml b/docker/api-base/compose.yml index 9ce5386..e27bbaa 100644 --- a/docker/api-base/compose.yml +++ b/docker/api-base/compose.yml @@ -10,12 +10,15 @@ services: networks: - front - back - - prom-exporter environment: - TZ=Europe/Paris - API_TARGET=user - LOG_FILE_PATH=/var/log/log.log - JWT_SECRET=${JWT_SECRET} + - CORS_ORIGIN=${CORS_ORIGIN} + - USER_URL=${USER_URL} + - AUTH_URL=${AUTH_URL} + - SCORE_URL=${SCORE_URL} restart: unless-stopped auth-api: container_name: transcendence-api-auth @@ -28,13 +31,38 @@ services: networks: - front - back - - prom-exporter environment: - TZ=Europe/Paris - GOOGLE_CALLBACK_URL=${GOOGLE_CALLBACK_URL} - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} + - CALLBACK_REDIR=${CALLBACK_REDIR} - API_TARGET=auth - LOG_FILE_PATH=/var/log/log.log - JWT_SECRET=${JWT_SECRET} + - CORS_ORIGIN=${CORS_ORIGIN} + - USER_URL=${USER_URL} + - AUTH_URL=${AUTH_URL} + - SCORE_URL=${SCORE_URL} + restart: unless-stopped + scorestore-api: + container_name: transcendence-api-scoreStore + build: + dockerfile: docker/api-base/Dockerfile + context: ../../ + volumes: + - db-scoreStore:/db + - log-scoreStore:/var/log + networks: + - back + environment: + - TZ=Europe/Paris + - API_TARGET=scoreStore + - LOG_FILE_PATH=/var/log/log.log + - AVAX_PRIVATE_KEY=${AVAX_PRIVATE_KEY} + - AVAX_RPC_URL=${AVAX_RPC_URL} + - AVAX_CONTRACT_ADDR=${AVAX_CONTRACT_ADDR} + - USER_URL=${USER_URL} + - AUTH_URL=${AUTH_URL} + - SCORE_URL=${SCORE_URL} restart: unless-stopped diff --git a/docker/front/Dockerfile b/docker/front/Dockerfile index b8f2ce2..36fc0e5 100644 --- a/docker/front/Dockerfile +++ b/docker/front/Dockerfile @@ -13,6 +13,9 @@ RUN cd /build \ FROM node:lts-alpine AS builder-vite +ARG VITE_USER_URL +ARG VITE_AUTH_URL + RUN npm install -g pnpm WORKDIR /app @@ -24,8 +27,8 @@ RUN pnpm install --frozen-lockfile COPY vite.config.js tailwind.config.js ./ COPY src ./src -RUN pnpm vite build - +RUN VITE_USER_URL=${VITE_USER_URL} VITE_AUTH_URL=${VITE_AUTH_URL}\ + pnpm vite build FROM alpine:3.22 diff --git a/docker/front/compose.yml b/docker/front/compose.yml index 72b2b67..a85d142 100644 --- a/docker/front/compose.yml +++ b/docker/front/compose.yml @@ -4,6 +4,9 @@ services: build: dockerfile: docker/front/Dockerfile context: ../../ + args: + - VITE_USER_URL=${VITE_USER_URL} + - VITE_AUTH_URL=${VITE_AUTH_URL} environment: - TZ=Europe/Paris networks: diff --git a/docker/monitoring/graphana/dashboards/11159_rev1.json.disabled b/docker/monitoring/graphana/dashboards/11159_rev1.json.disabled deleted file mode 100644 index c7b96f6..0000000 --- a/docker/monitoring/graphana/dashboards/11159_rev1.json.disabled +++ /dev/null @@ -1,933 +0,0 @@ -{ - "__inputs": [ - { - "name": "DS_PROMETHEUS", - "label": "prometheus", - "description": "", - "type": "datasource", - "pluginId": "prometheus", - "pluginName": "Prometheus" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "6.0.1" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "5.0.0" - }, - { - "type": "datasource", - "id": "prometheus", - "name": "Prometheus", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "singlestat", - "name": "Singlestat", - "version": "5.0.0" - } - ], - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "description": "node.js prometheus client basic metrics", - "editable": true, - "gnetId": 11159, - "graphTooltip": 0, - "id": null, - "iteration": 1573392431370, - "links": [], - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "gridPos": { - "h": 7, - "w": 10, - "x": 0, - "y": 0 - }, - "id": 6, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "paceLength": 10, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "irate(process_cpu_user_seconds_total{instance=~\"$instance\"}[2m]) * 100", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "User CPU - {{instance}}", - "refId": "A" - }, - { - "expr": "irate(process_cpu_system_seconds_total{instance=~\"$instance\"}[2m]) * 100", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "Sys CPU - {{instance}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Process CPU Usage", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "percent", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "gridPos": { - "h": 7, - "w": 9, - "x": 10, - "y": 0 - }, - "id": 8, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "paceLength": 10, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "nodejs_eventloop_lag_seconds{instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{instance}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Event Loop Lag", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": false, - "colors": [ - "#299c46", - "rgba(237, 129, 40, 0.89)", - "#d44a3a" - ], - "datasource": "${DS_PROMETHEUS}", - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "gridPos": { - "h": 3, - "w": 5, - "x": 19, - "y": 0 - }, - "id": 2, - "interval": "", - "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false - }, - "tableColumn": "__name__", - "targets": [ - { - "expr": "sum(nodejs_version_info{instance=~\"$instance\"}) by (version)", - "format": "time_series", - "instant": false, - "interval": "", - "intervalFactor": 1, - "legendFormat": "{{version}}", - "refId": "A" - } - ], - "thresholds": "", - "timeFrom": null, - "timeShift": null, - "title": "Node.js Version", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "name" - }, - { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": false, - "colors": [ - "#299c46", - "rgba(237, 129, 40, 0.89)", - "#d44a3a" - ], - "datasource": "${DS_PROMETHEUS}", - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "gridPos": { - "h": 4, - "w": 5, - "x": 19, - "y": 3 - }, - "id": 4, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "#F2495C", - "show": true - }, - "tableColumn": "", - "targets": [ - { - "expr": "sum(changes(process_start_time_seconds{instance=~\"$instance\"}[1m]))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{instance}}", - "refId": "A" - } - ], - "thresholds": "", - "timeFrom": null, - "timeShift": null, - "title": "Process Restart Times", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "current" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "gridPos": { - "h": 7, - "w": 16, - "x": 0, - "y": 7 - }, - "id": 7, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": true, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "paceLength": 10, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "process_resident_memory_bytes{instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "Process Memory - {{instance}}", - "refId": "A" - }, - { - "expr": "nodejs_heap_size_total_bytes{instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "Heap Total - {{instance}}", - "refId": "B" - }, - { - "expr": "nodejs_heap_size_used_bytes{instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "Heap Used - {{instance}}", - "refId": "C" - }, - { - "expr": "nodejs_external_memory_bytes{instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "External Memory - {{instance}}", - "refId": "D" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Process Memory Usage", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "bytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 16, - "y": 7 - }, - "id": 9, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "paceLength": 10, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "nodejs_active_handles_total{instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "Active Handler - {{instance}}", - "refId": "A" - }, - { - "expr": "nodejs_active_requests_total{instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "Active Request - {{instance}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Active Handlers/Requests Total", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "gridPos": { - "h": 8, - "w": 8, - "x": 0, - "y": 14 - }, - "id": 10, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": true, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "paceLength": 10, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "nodejs_heap_space_size_total_bytes{instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "Heap Total - {{instance}} - {{space}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Heap Total Detail", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "bytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "gridPos": { - "h": 8, - "w": 8, - "x": 8, - "y": 14 - }, - "id": 11, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": true, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "paceLength": 10, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "nodejs_heap_space_size_used_bytes{instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "Heap Used - {{instance}} - {{space}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Heap Used Detail", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "bytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "gridPos": { - "h": 8, - "w": 8, - "x": 16, - "y": 14 - }, - "id": 12, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": true, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "paceLength": 10, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "nodejs_heap_space_size_available_bytes{instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "Heap Used - {{instance}} - {{space}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Heap Available Detail", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "bytes", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "schemaVersion": 18, - "style": "dark", - "tags": [ - "nodejs" - ], - "templating": { - "list": [ - { - "allValue": null, - "current": {}, - "datasource": "${DS_PROMETHEUS}", - "definition": "label_values(nodejs_version_info, instance)", - "hide": 0, - "includeAll": true, - "label": "instance", - "multi": true, - "name": "instance", - "options": [], - "query": "label_values(nodejs_version_info, instance)", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 1, - "tagValuesQuery": "", - "tags": [], - "tagsQuery": "", - "type": "query", - "useTags": false - } - ] - }, - "time": { - "from": "now-1h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "timezone": "", - "title": "NodeJS Application Dashboard", - "uid": "PTSqcpJWk", - "version": 4 -} \ No newline at end of file diff --git a/docker/monitoring/graphana/dashboards/14900_rev2.json b/docker/monitoring/graphana/dashboards/14900_rev2.json index 60e47b5..2f58ee9 100644 --- a/docker/monitoring/graphana/dashboards/14900_rev2.json +++ b/docker/monitoring/graphana/dashboards/14900_rev2.json @@ -87,215 +87,6 @@ "title": "General Metrics", "type": "row" }, - { - "aliasColors": { - "172.31.0.131:9273": "dark-green" - }, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fill": 10, - "fillGradient": 0, - "gridPos": { - "h": 7, - "w": 14, - "x": 0, - "y": 1 - }, - "hiddenSeries": false, - "id": 18, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "100 - (cpu_usage_idle{instance=~ \"$instance*\",cpu=\"cpu-total\"})", - "interval": "", - "legendFormat": "{{instance}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "CPU Usage", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:661", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:662", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "datasource": "$datasource", - "fieldConfig": { - "defaults": { - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "semi-dark-orange", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 5, - "x": 14, - "y": 1 - }, - "id": 20, - "options": { - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "mean" - ], - "values": false - }, - "showThresholdLabels": false, - "showThresholdMarkers": true - }, - "pluginVersion": "7.0.1", - "targets": [ - { - "expr": "100 - (cpu_usage_idle{instance=~ \"$instance*\",cpu=\"cpu-total\"})", - "interval": "", - "legendFormat": "{{instance}}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Current CPU Utilization %", - "type": "gauge" - }, - { - "datasource": "$datasource", - "fieldConfig": { - "defaults": { - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "light-red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 5, - "x": 19, - "y": 1 - }, - "id": 16, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "mean" - ], - "values": false - } - }, - "pluginVersion": "7.0.1", - "targets": [ - { - "expr": "sum(mem_used{instance=~\"$instance\"})", - "interval": "", - "legendFormat": "{{ host }}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Current Memory Utilization", - "type": "stat" - }, { "aliasColors": { "mem_used": "rgb(214, 141, 89)", @@ -556,1389 +347,6 @@ "align": false, "alignLevel": null } - }, - { - "collapsed": false, - "datasource": "${DS_PROMETHEUS.INTERNAL-ODMONT.COM}", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 16 - }, - "id": 24, - "panels": [], - "title": "Nginx Requests Status Code", - "type": "row" - }, - { - "aliasColors": { - "172.31.11.8:9273": "semi-dark-yellow" - }, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "decimals": null, - "description": "Requests with 200 Status Code", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 17, - "x": 0, - "y": 17 - }, - "hiddenSeries": false, - "id": 28, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 2, - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "sum_over_time(nginxlog_resp_bytes{instance=~\"$instance*\", resp_code=~\"20.\"}[5m])", - "interval": "", - "legendFormat": "{{request}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Response 2XX / 5m", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:91", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:92", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "datasource": "$datasource", - "description": "Total number of request in 24 hours for 200 response by nginx server", - "fieldConfig": { - "defaults": { - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "yellow", - "value": null - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 9, - "w": 7, - "x": 17, - "y": 17 - }, - "id": 39, - "options": { - "displayMode": "gradient", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "mean" - ], - "values": false - }, - "showUnfilled": true - }, - "pluginVersion": "7.0.1", - "targets": [ - { - "expr": "sum_over_time(nginxlog_resp_bytes{instance=~\"$instance*\", resp_code=~\"200\"}[24h])", - "interval": "", - "legendFormat": "{{request}}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Total Response 200 Req. [24h]", - "type": "bargauge" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "description": "Requests with 4XX Status Code", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 17, - "x": 0, - "y": 26 - }, - "hiddenSeries": false, - "id": 30, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 2, - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "sum_over_time(nginxlog_resp_bytes{instance=~\"$instance*\", resp_code=~\"40.\"}[5m])", - "interval": "", - "legendFormat": "{{request}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Response 4XX /5m", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:167", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:168", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "datasource": "$datasource", - "description": "Total number of request in 24 hours for 404 response from nginx server", - "fieldConfig": { - "defaults": { - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "light-red", - "value": null - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 9, - "w": 7, - "x": 17, - "y": 26 - }, - "id": 40, - "options": { - "displayMode": "gradient", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "mean" - ], - "values": false - }, - "showUnfilled": true - }, - "pluginVersion": "7.0.1", - "targets": [ - { - "expr": "sum_over_time(nginxlog_resp_bytes{instance=~\"$instance*\", resp_code=~\"404\"}[5m])", - "interval": "", - "legendFormat": "{{request}}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Total Response 404 Req. [24h]", - "type": "bargauge" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "description": "Requests with 3XX Status Code", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 9, - "x": 0, - "y": 35 - }, - "hiddenSeries": false, - "id": 31, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "sum_over_time(nginxlog_resp_bytes{instance=~\"$instance*\", resp_code=~\"30.\"}[5m])", - "interval": "", - "legendFormat": "{{instance}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Response 3XX", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:929", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:930", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "description": "Requests with 5XX Status Code", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 10, - "x": 9, - "y": 35 - }, - "hiddenSeries": false, - "id": 32, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "sum_over_time(nginxlog_resp_bytes{instance=~\"$instance*\", resp_code=~\"50.\"}[5m])", - "interval": "", - "legendFormat": "{{instance}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Response 5XX", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:1003", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:1004", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "datasource": "$datasource", - "fieldConfig": { - "defaults": { - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 5, - "x": 19, - "y": 35 - }, - "id": 41, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "mean" - ], - "values": false - } - }, - "pluginVersion": "7.0.1", - "targets": [ - { - "expr": "sum_over_time(nginxlog_resp_bytes{instance=~\"$instance*\", resp_code=~\"503\"}[5m])", - "interval": "", - "legendFormat": "{{request}}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Total Request 503 Resonse", - "type": "stat" - }, - { - "collapsed": false, - "datasource": "${DS_PROMETHEUS.INTERNAL-ODMONT.COM}", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 43 - }, - "id": 22, - "panels": [], - "title": "Nginx Metrics", - "type": "row" - }, - { - "aliasColors": { - "172.31.0.131:9273": "super-light-purple", - "{__name__=\"nginx_accepts\", env=\"prod\", exported_env=\"DEV\", instance=\"172.31.0.131:9273\", job=\"telegraf\", port=\"80\", server=\"localhost\"}": "purple" - }, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "description": "The total number of accepted connections from clients since the nginx master process started. Note that reloading configurations or restarting worker processes will not reset this metric. If you terminate and restart the master process, you will reset the metric.\n\n", - "fieldConfig": { - "defaults": { - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "rgb(242, 61, 5)", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "fill": 3, - "fillGradient": 0, - "gridPos": { - "h": 7, - "w": 9, - "x": 0, - "y": 44 - }, - "hiddenSeries": false, - "id": 4, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "hideEmpty": false, - "hideZero": false, - "max": true, - "min": true, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 3, - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pluginVersion": "7.0.1", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "nginx_accepts{instance=~\"$instance*\"}", - "interval": "", - "legendFormat": "{{ instance }}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Nginx Connections Accepted", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:450", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:451", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "datasource": "$datasource", - "description": "The current number of connections from clients where nginx is writing a response back to the client.\n", - "fieldConfig": { - "defaults": { - "custom": { - "align": null - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 4, - "x": 9, - "y": 44 - }, - "id": 12, - "options": { - "colorMode": "value", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "mean" - ], - "values": false - } - }, - "pluginVersion": "7.0.1", - "targets": [ - { - "expr": "nginx_writing{instance=~\"$instance*\"}\t", - "interval": "", - "legendFormat": "{{ host }}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Nginx Writing", - "type": "stat" - }, - { - "datasource": "$datasource", - "description": "The current number of active (accepted) connections from clients. Includes all connections with the statuses Idle / Waiting, Reading, and Writing.", - "fieldConfig": { - "defaults": { - "custom": { - "align": null - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "dark-orange", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 7, - "x": 13, - "y": 44 - }, - "id": 6, - "options": { - "displayMode": "lcd", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "min" - ], - "values": false - }, - "showUnfilled": true - }, - "pluginVersion": "7.0.1", - "targets": [ - { - "expr": "nginx_active{instance=~\"$instance*\"}\t", - "interval": "", - "legendFormat": "{{instance }}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Nginx Active Connections[accepted]", - "type": "bargauge" - }, - { - "datasource": "$datasource", - "description": "The current number of connections from clients that are in the Idle / Waiting state.", - "fieldConfig": { - "defaults": { - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "semi-dark-blue", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 4, - "x": 20, - "y": 44 - }, - "id": 10, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "min" - ], - "values": false - } - }, - "pluginVersion": "7.0.1", - "targets": [ - { - "expr": "nginx_waiting{instance=~\"$instance*\"}\t", - "interval": "", - "legendFormat": "{{ instance}}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Nginx Waiting Connections", - "type": "stat" - }, - { - "aliasColors": { - "172.31.0.131:9273": "rgb(219, 237, 9)", - "{__name__=\"nginx_handled\", env=\"prod\", exported_env=\"DEV\", instance=\"172.31.0.131:9273\", job=\"telegraf\", port=\"80\", server=\"localhost\"}": "rgb(208, 240, 17)" - }, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "description": "The total number of handled connections from clients since the nginx master process started. This will be lower than accepted only in cases where a connection is dropped before it is handled.", - "fieldConfig": { - "defaults": { - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "fill": 3, - "fillGradient": 0, - "gridPos": { - "h": 7, - "w": 10, - "x": 0, - "y": 51 - }, - "hiddenSeries": false, - "id": 14, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 3, - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pluginVersion": "7.0.1", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "nginx_handled{instance=~\"$instance*\"}\t", - "interval": "", - "legendFormat": "{{ instance }}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Handled Request", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:1528", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:1529", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": { - "172.31.0.131:9273": "rgb(250, 0, 225)", - "{__name__=\"nginx_requests\", env=\"prod\", exported_env=\"DEV\", instance=\"172.31.0.131:9273\", job=\"telegraf\", port=\"80\", server=\"localhost\"}": "orange" - }, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "decimals": null, - "description": "The total number of client requests since the nginx master process started. A request is an application-level (HTTP, etc.) event. It occurs when a client requests a resource via the application protocol. A single connection can (and often does) make many requests. So most of the time, there are more requests than accepted/handled connections.", - "fieldConfig": { - "defaults": { - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "fill": 3, - "fillGradient": 0, - "gridPos": { - "h": 7, - "w": 10, - "x": 10, - "y": 51 - }, - "hiddenSeries": false, - "id": 2, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "hideEmpty": false, - "hideZero": false, - "max": false, - "min": true, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 3, - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pluginVersion": "7.0.1", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "nginx_requests{instance=~\"$instance*\"}\t", - "interval": "", - "legendFormat": "{{ instance }}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Nginx Requests", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:119", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:120", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "datasource": "${DS_PROMETHEUS.INTERNAL-ODMONT.COM}", - "description": "The current number of (accepted) connections from clients where nginx is reading the request. Measured at the time the status module was queried.", - "fieldConfig": { - "defaults": { - "custom": {}, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "semi-dark-green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 4, - "x": 20, - "y": 51 - }, - "id": 8, - "options": { - "colorMode": "value", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "mean" - ], - "values": false - } - }, - "pluginVersion": "7.0.1", - "targets": [ - { - "expr": "nginx_reading{instance=~\"$instance*\"}", - "interval": "", - "legendFormat": "{{ host }}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "nginx_reading", - "type": "stat" - }, - { - "datasource": "$datasource", - "fieldConfig": { - "defaults": { - "custom": { - "align": "center", - "displayMode": "auto" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Time" - }, - "properties": [ - { - "id": "custom.width", - "value": 144 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "agent" - }, - "properties": [ - { - "id": "custom.width", - "value": 173 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "auth" - }, - "properties": [ - { - "id": "custom.width", - "value": 84 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "client_ip" - }, - "properties": [ - { - "id": "custom.width", - "value": 143 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "env" - }, - "properties": [ - { - "id": "custom.width", - "value": 20 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "exported_env" - }, - "properties": [ - { - "id": "custom.width", - "value": 122 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ident" - }, - "properties": [ - { - "id": "custom.width", - "value": 74 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "job" - }, - "properties": [ - { - "id": "custom.width", - "value": 53 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "path" - }, - "properties": [ - { - "id": "custom.width", - "value": 151 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "referrer" - }, - "properties": [ - { - "id": "custom.width", - "value": 82 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "resp_code" - }, - "properties": [ - { - "id": "custom.width", - "value": 94 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "verb" - }, - "properties": [ - { - "id": "custom.width", - "value": 48 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "request" - }, - "properties": [ - { - "id": "custom.width", - "value": 208 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Value" - }, - "properties": [ - { - "id": "custom.width", - "value": 29 - } - ] - } - ] - }, - "gridPos": { - "h": 16, - "w": 24, - "x": 0, - "y": 58 - }, - "id": 34, - "options": { - "showHeader": true, - "sortBy": [] - }, - "pluginVersion": "7.0.1", - "repeat": null, - "repeatDirection": "h", - "targets": [ - { - "expr": "nginxlog_resp_bytes{instance=~\"$instance*\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Each Request Detail", - "transformations": [ - { - "id": "labelsToFields", - "options": {} - } - ], - "type": "table" } ], "refresh": false, @@ -2071,4 +479,4 @@ "uid": "4DFTt9Wnk", "version": 6, "description": "Grafana Dashboard for Nginx Web Server." -} \ No newline at end of file +} diff --git a/docker/monitoring/prometheus/config/prometheus.yml b/docker/monitoring/prometheus/config/prometheus.yml index e3789e5..59f9fc5 100644 --- a/docker/monitoring/prometheus/config/prometheus.yml +++ b/docker/monitoring/prometheus/config/prometheus.yml @@ -23,8 +23,3 @@ scrape_configs: - job_name: 'node-exporter' static_configs: - targets: ['node-exporter:9100'] - - # - job_name: 'nodejs' - # static_configs: - # - targets: ['transcendence-api-auth:3000'] - # - targets: ['transcendence-api-user:3000'] diff --git a/docker/proxy/Dockerfile b/docker/proxy/Dockerfile index c46fb7c..75e20c7 100644 --- a/docker/proxy/Dockerfile +++ b/docker/proxy/Dockerfile @@ -1,18 +1,3 @@ -FROM node:lts-alpine AS builder - -RUN npm install -g pnpm - -WORKDIR /app - -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ - -RUN pnpm install --frozen-lockfile - -COPY vite.config.js tailwind.config.js ./ -COPY src ./src - -RUN pnpm vite build - FROM owasp/modsecurity-crs:nginx-alpine RUN mkdir -p /etc/nginx/modsecurity.d \ @@ -23,7 +8,7 @@ COPY docker/proxy/config/default.conf.template \ COPY --chmod=755 docker/proxy/entry/ssl-cert.sh /docker-entrypoint.d/ssl-cert.sh -COPY --from=builder /app/dist /usr/share/nginx/html +# COPY --from=builder /app/dist /usr/share/nginx/html USER root RUN mkdir -p /var/log/front diff --git a/docker/proxy/Dockerfile.prod b/docker/proxy/Dockerfile.prod index 5c83136..dd23810 100644 --- a/docker/proxy/Dockerfile.prod +++ b/docker/proxy/Dockerfile.prod @@ -1,18 +1,3 @@ -FROM node:lts-alpine AS builder - -RUN npm install -g pnpm - -WORKDIR /app - -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ - -RUN pnpm install --frozen-lockfile - -COPY vite.config.js tailwind.config.js ./ -COPY src ./src - -RUN pnpm vite build - FROM owasp/modsecurity-crs:nginx-alpine RUN mkdir -p /etc/nginx/modsecurity.d \ @@ -21,10 +6,6 @@ RUN mkdir -p /etc/nginx/modsecurity.d \ COPY docker/proxy/config/default.prod.conf.template \ /etc/nginx/templates/conf.d/default.conf.template -COPY --chmod=755 docker/proxy/entry/ssl-cert.sh /docker-entrypoint.d/ssl-cert.sh - -COPY --from=builder /app/dist /usr/share/nginx/html - USER root RUN mkdir -p /var/log/front RUN touch /var/log/front/err.log /var/log/front/log.log diff --git a/docker/proxy/config/default.conf.template b/docker/proxy/config/default.conf.template index 0f65e5e..c8dc39b 100644 --- a/docker/proxy/config/default.conf.template +++ b/docker/proxy/config/default.conf.template @@ -19,11 +19,19 @@ server { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; location / { - proxy_pass http://transcendence-webserv:80/; + proxy_pass http://transcendence-webserv:80; + proxy_http_version 1.1; + proxy_redirect off; + rewrite ^ / break; + } + + location /assets/ { + proxy_pass http://transcendence-webserv:80/assets/; proxy_http_version 1.1; } location /api/v1/user/ { + modsecurity off; proxy_pass http://transcendence-api-user:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -35,6 +43,7 @@ server { } location /api/v1/auth/ { + modsecurity off; proxy_pass http://transcendence-api-auth:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/docker/volumes.yml b/docker/volumes.yml index f05e822..188877f 100644 --- a/docker/volumes.yml +++ b/docker/volumes.yml @@ -5,9 +5,13 @@ volumes: name: transcendence-api-auth-db db-user: name: transcendence-api-user-db + db-scoreStore: + name: transcendence-api-scoreStore log-auth: name: transcendence-api-auth-log log-user: name: transcendence-api-user-log log-nginx: name: transcendence-front-log + log-scoreStore: + name: transcendence-scoreStore-log diff --git a/flake.lock b/flake.lock index 782aaad..dade030 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1753250450, - "narHash": "sha256-i+CQV2rPmP8wHxj0aq4siYyohHwVlsh40kV89f3nw1s=", + "lastModified": 1756542300, + "narHash": "sha256-tlOn88coG5fzdyqz6R93SQL5Gpq+m/DsWpekNFhqPQk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "fc02ee70efb805d3b2865908a13ddd4474557ecf", + "rev": "d7600c775f877cd87b4f5a831c28aa94137377aa", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index b61cb1a..fb52ec3 100644 --- a/flake.nix +++ b/flake.nix @@ -39,7 +39,7 @@ nodejs_22 pnpm just - foundry + foundry ]; shellHook = '' if [ ! -d node_modules/ ]; then @@ -50,6 +50,7 @@ echo Installing foundry env forge i fi + alias jarvis=just export PATH+=:$(pwd)/node_modules/.bin echo entering ft_trans env ''; diff --git a/package.json b/package.json index 7eedbe9..77c60ce 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "dependencies": { "@avalabs/avalanchejs": "^5.0.0", "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^11.1.0", "@fastify/env": "^5.0.2", "@fastify/jwt": "^9.1.0", "axios": "^1.10.0", @@ -13,6 +14,7 @@ "fastify-cli": "^7.4.0", "pino": "^9.7.0", "prom-client": "^15.1.3", + "sharp": "^0.34.4", "solhint": "^6.0.0" }, "type": "module", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91f44c7..ce70826 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@fastify/cookie': specifier: ^11.0.2 version: 11.0.2 + '@fastify/cors': + specifier: ^11.1.0 + version: 11.1.0 '@fastify/env': specifier: ^5.0.2 version: 5.0.2 @@ -47,6 +50,9 @@ importers: prom-client: specifier: ^15.1.3 version: 15.1.3 + sharp: + specifier: ^0.34.4 + version: 0.34.4 solhint: specifier: ^6.0.0 version: 6.0.0(typescript@5.8.3) @@ -88,6 +94,9 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@esbuild/aix-ppc64@0.25.6': resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} engines: {node: '>=18'} @@ -255,6 +264,9 @@ packages: '@fastify/cookie@11.0.2': resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} + '@fastify/cors@11.1.0': + resolution: {integrity: sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA==} + '@fastify/deepmerge@2.0.2': resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==} @@ -283,6 +295,132 @@ packages: resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} engines: {node: '>=10.10.0'} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.4': + resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.4': + resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.3': + resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.3': + resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.3': + resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.3': + resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.3': + resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.3': + resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.3': + resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.4': + resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.4': + resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.4': + resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.4': + resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.4': + resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.4': + resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.4': + resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.4': + resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.4': + resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.4': + resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.4': + resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -753,6 +891,10 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dotenv-expand@10.0.0: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} @@ -1452,6 +1594,10 @@ packages: set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + sharp@0.34.4: + resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -1667,6 +1813,11 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.7.0 + optional: true + '@esbuild/aix-ppc64@0.25.6': optional: true @@ -1758,6 +1909,11 @@ snapshots: cookie: 1.0.2 fastify-plugin: 5.0.1 + '@fastify/cors@11.1.0': + dependencies: + fastify-plugin: 5.0.1 + toad-cache: 3.7.0 + '@fastify/deepmerge@2.0.2': {} '@fastify/env@5.0.2': @@ -1792,6 +1948,94 @@ snapshots: '@humanwhocodes/momoa@2.0.4': {} + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.3 + optional: true + + '@img/sharp-darwin-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.3': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.3': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + optional: true + + '@img/sharp-linux-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.3 + optional: true + + '@img/sharp-linux-arm@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.3 + optional: true + + '@img/sharp-linux-ppc64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.3 + optional: true + + '@img/sharp-linux-s390x@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.3 + optional: true + + '@img/sharp-linux-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.3 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + optional: true + + '@img/sharp-wasm32@0.34.4': + dependencies: + '@emnapi/runtime': 1.5.0 + optional: true + + '@img/sharp-win32-arm64@0.34.4': + optional: true + + '@img/sharp-win32-ia32@0.34.4': + optional: true + + '@img/sharp-win32-x64@0.34.4': + optional: true + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -2183,6 +2427,8 @@ snapshots: detect-libc@2.0.4: {} + detect-libc@2.1.2: {} + dotenv-expand@10.0.0: {} dotenv@16.6.1: {} @@ -2914,6 +3160,35 @@ snapshots: set-cookie-parser@2.7.1: {} + sharp@0.34.4: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.4 + '@img/sharp-darwin-x64': 0.34.4 + '@img/sharp-libvips-darwin-arm64': 1.2.3 + '@img/sharp-libvips-darwin-x64': 1.2.3 + '@img/sharp-libvips-linux-arm': 1.2.3 + '@img/sharp-libvips-linux-arm64': 1.2.3 + '@img/sharp-libvips-linux-ppc64': 1.2.3 + '@img/sharp-libvips-linux-s390x': 1.2.3 + '@img/sharp-libvips-linux-x64': 1.2.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + '@img/sharp-linux-arm': 0.34.4 + '@img/sharp-linux-arm64': 0.34.4 + '@img/sharp-linux-ppc64': 0.34.4 + '@img/sharp-linux-s390x': 0.34.4 + '@img/sharp-linux-x64': 0.34.4 + '@img/sharp-linuxmusl-arm64': 0.34.4 + '@img/sharp-linuxmusl-x64': 0.34.4 + '@img/sharp-wasm32': 0.34.4 + '@img/sharp-win32-arm64': 0.34.4 + '@img/sharp-win32-ia32': 0.34.4 + '@img/sharp-win32-x64': 0.34.4 + simple-concat@1.0.1: {} simple-get@4.0.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a8eee40..8a8cca4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,3 +5,4 @@ ignoredBuiltDependencies: onlyBuiltDependencies: - better-sqlite3 + - sharp diff --git a/src/api/auth/default.js b/src/api/auth/default.js index 6a15651..ecba601 100644 --- a/src/api/auth/default.js +++ b/src/api/auth/default.js @@ -1,6 +1,8 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; +import cors from '@fastify/cors'; +import { totpCheck } from './totpCheck.js'; import { register } from './register.js'; import { login } from './login.js'; import { gRedir } from './gRedir.js'; @@ -10,6 +12,8 @@ import { gRegisterCallback } from './gRegisterCallback.js'; import { totpSetup } from './totpSetup.js'; import { totpDelete } from './totpDelete.js'; import { totpVerify } from './totpVerify.js'; +import { logout } from './logout.js'; +import { remove } from './remove.js'; const saltRounds = 10; export const appName = process.env.APP_NAME || 'knl_meowscendence'; @@ -22,6 +26,12 @@ authDB.prepareDB(); */ export default async function(fastify, options) { + fastify.register(cors, { + origin: process.env.CORS_ORIGIN || 'http://localhost:5173', + credentials: true, + methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"] + }); + fastify.register(fastifyJWT, { secret: process.env.JWT_SECRET || '123456789101112131415161718192021', cookie: { @@ -45,6 +55,9 @@ export default async function(fastify, options) { fastify.get('/me', { preHandler: [fastify.authenticate] }, async (request, reply) => { return { user: request.user }; }); + fastify.get('/2fa', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return totpCheck(request, reply); + }); // GOOGLE sign in fastify.get('/login/google', async (request, reply) => { @@ -107,4 +120,8 @@ export default async function(fastify, options) { } } }, async (request, reply) => { return register(request, reply, saltRounds, fastify); }); + + fastify.get('/logout', {}, async (request, reply) => { return logout(reply, fastify); }) + + fastify.delete('/', { preHandler: fastify.authenticate }, async (request, reply) => { return remove(request, reply, fastify) }) } diff --git a/src/api/auth/gLogCallback.js b/src/api/auth/gLogCallback.js index 975b7d6..6b3c78b 100644 --- a/src/api/auth/gLogCallback.js +++ b/src/api/auth/gLogCallback.js @@ -37,7 +37,7 @@ export async function gLogCallback(request, reply, fastify) { return reply.code(400).send({ error: "User does not exist" }); } - const token = fastify.jwt.sign(user); + const token = fastify.jwt.sign({ user: user.username}); return reply .setCookie('token', token, { @@ -45,9 +45,7 @@ export async function gLogCallback(request, reply, fastify) { path: '/', secure: env !== 'development', sameSite: 'lax', - }) - .code(200) - .send({ msg: "Login successful" }); + }).redirect(process.env.CALLBACK_REDIR); } catch (error) { fastify.log.error(error); reply.code(500).send({ error: 'Internal server error' }); diff --git a/src/api/auth/gRegisterCallback.js b/src/api/auth/gRegisterCallback.js index f79542f..217fae5 100644 --- a/src/api/auth/gRegisterCallback.js +++ b/src/api/auth/gRegisterCallback.js @@ -1,6 +1,7 @@ import axios from 'axios' import authDB from '../../utils/authDB.js'; +import { authUserCreate } from '../../utils/authUserCreate.js'; var env = process.env.NODE_ENV || 'development'; @@ -46,7 +47,9 @@ export async function gRegisterCallback(request, reply, fastify) { authDB.addUser(user.username, ''); - const token = fastify.jwt.sign(user); + authUserCreate(user.username, fastify) + + const token = fastify.jwt.sign({ user: user.username}); return reply .setCookie('token', token, { @@ -54,9 +57,7 @@ export async function gRegisterCallback(request, reply, fastify) { path: '/', secure: env !== 'development', sameSite: 'lax', - }) - .code(200) - .send({ msg: "Register successful" }); + }).redirect(process.env.CALLBACK_REDIR); } catch (error) { fastify.log.error(error); reply.code(500).send({ error: 'Internal server error' }); diff --git a/src/api/auth/login.js b/src/api/auth/login.js index 2580383..83d9a76 100644 --- a/src/api/auth/login.js +++ b/src/api/auth/login.js @@ -37,8 +37,8 @@ export async function login(request, reply, fastify) { const userTOTP = authDB.getUser(user); if (userTOTP.totpEnabled == 1) { - if (!request.body.token){ - return reply.code(401).send({ error: 'Invalid 2FA token' }); + if (!request.body.token) { + return reply.code(402).send({ error: 'Please specify a 2fa token' }); } const isValid = verifyTOTP(userTOTP.totpHash, request.body.token); if (!isValid) { diff --git a/src/api/auth/logout.js b/src/api/auth/logout.js new file mode 100644 index 0000000..b3c57ef --- /dev/null +++ b/src/api/auth/logout.js @@ -0,0 +1,18 @@ +/** + * @async + * @param {import("fastify").FastifyReply} reply + * @param {import("fastify").FastifyInstance} fastify + * + * @returns {import("fastify").FastifyReply} + */ +export async function logout(reply, fastify) { + try { + return reply + .code(200) + .clearCookie("token") + .send({ msg: "Logout successful" }); + } catch { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/auth/register.js b/src/api/auth/register.js index 7463452..5e10ff1 100644 --- a/src/api/auth/register.js +++ b/src/api/auth/register.js @@ -2,6 +2,7 @@ import bcrypt from 'bcrypt'; import { isValidString } from '../../utils/authUtils.js'; import authDB from '../../utils/authDB.js'; +import { authUserCreate } from '../../utils/authUserCreate.js'; var env = process.env.NODE_ENV || 'development'; @@ -36,6 +37,8 @@ export async function register(request, reply, saltRounds, fastify) { const hash = await bcrypt.hash(password, saltRounds); authDB.addUser(user, hash); + authUserCreate(user, fastify) + const token = fastify.jwt.sign({ user }); return reply diff --git a/src/api/auth/remove.js b/src/api/auth/remove.js new file mode 100644 index 0000000..ea1cbce --- /dev/null +++ b/src/api/auth/remove.js @@ -0,0 +1,35 @@ +import authDB from '../../utils/authDB.js'; +import { authUserRemove } from '../../utils/authUserRemove.js'; + +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} reply + * @param {import('fastify').FastifyInstance} fastify + */ +export async function remove(request, reply, fastify) { + try { + const user = request.user; + + if (authDB.RESERVED_USERNAMES.includes(user)) { + return reply.code(400).send({ error: 'Reserved username' }); + } + + if (authDB.checkUser(user) === false) { + return reply.code(400).send({ error: "User does not exist" }); + } + + authDB.rmUser(user) + + authUserRemove(user, fastify) + + return reply + .code(200) + .clearCookie("token") + .send({ + msg: "User successfully deleted" + }) + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/auth/totpCheck.js b/src/api/auth/totpCheck.js new file mode 100644 index 0000000..d76a12e --- /dev/null +++ b/src/api/auth/totpCheck.js @@ -0,0 +1,24 @@ +import authDB from '../../utils/authDB.js'; + +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} reply + */ +export async function totpCheck(request, reply) { + try { + const user = request.user; + + if (authDB.checkUser(user) === false) { + return reply.code(400).send({ error: "User does not exist" }); + } + + return reply + .code(200) + .send({ + totp: authDB.isTOTPEnabled(user) + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/scoreStore/addTx.js b/src/api/scoreStore/addTx.js index 413086b..e9682fa 100644 --- a/src/api/scoreStore/addTx.js +++ b/src/api/scoreStore/addTx.js @@ -9,12 +9,11 @@ import { callAddScore, callLastId } from "../../utils/scoreStore_contract.js"; */ export async function addTx(request, reply, fastify) { try { - const id = await callLastId(); - const tx = callAddScore(request.body.p1, request.body.p2, request.body.p1Score, request.body.p2Score); + const {tx, id} = await callAddScore(request.body.p1, request.body.p2, request.body.p1Score, request.body.p2Score); - tx.then(tx => { - scoreDB.addTx(id, tx.hash); - }); + scoreDB.addTx(id, tx.hash); + // tx.then(tx => { + // }); return reply.code(200).send({ id: Number(id) diff --git a/src/api/scoreStore/default.js b/src/api/scoreStore/default.js index 954fcc8..c6b39ea 100644 --- a/src/api/scoreStore/default.js +++ b/src/api/scoreStore/default.js @@ -20,7 +20,7 @@ export default async function(fastify, options) { required: ['p1', 'p2', 'p1Score', 'p2Score'], properties: { p1: { type: 'string', minLength: 1 }, - p2: { type: 'string', minLength: 1 }, + p2: { type: 'string', minLength: 0 }, p1Score: { type: 'integer', minimum: 0 }, p2Score: { type: 'integer', minimum: 0 }, } diff --git a/src/api/user/dAvatar.js b/src/api/user/dAvatar.js new file mode 100644 index 0000000..cfbea1c --- /dev/null +++ b/src/api/user/dAvatar.js @@ -0,0 +1,24 @@ +export async function dAvatar(request, reply, fastify, getUserInfo, getAvatarId, deleteAvatarId, deleteImage) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + if (!getUserInfo.get(userId)) { + return reply.cose(404).send({ error: "User does not exist" }); + } + const imageId = getAvatarId.get(userId); + if (imageId.avatarId === -1) { + return reply.code(404).send({ error: "User does not have an avatar" }); + } + deleteImage.run(imageId.avatarId); + deleteAvatarId.run(userId); + return reply.code(200).send({ msg: "Avatar deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dFriend.js b/src/api/user/dFriend.js new file mode 100644 index 0000000..1f3fddd --- /dev/null +++ b/src/api/user/dFriend.js @@ -0,0 +1,23 @@ +export async function dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + if (request.user !== 'admin' && request.user !== userId) { + return reply.code(401).send({ error: "Unauthorized" }); + } + const friendId = request.params.friendId; + if (!getFriend.get(userId, friendId)) { + return reply.code(404).send({ error: "Friend does not exist" }); + } + deleteFriend.run(userId, friendId); + return reply.code(200).send({ msg: "Friend deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dFriends.js b/src/api/user/dFriends.js new file mode 100644 index 0000000..11d4423 --- /dev/null +++ b/src/api/user/dFriends.js @@ -0,0 +1,16 @@ +export async function dFriends(request, reply, fastify, getUserInfo, deleteFriends) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + deleteFriends.run(userId); + return reply.code(200).send({ msg: "Friends deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dMatchHistory.js b/src/api/user/dMatchHistory.js new file mode 100644 index 0000000..241813b --- /dev/null +++ b/src/api/user/dMatchHistory.js @@ -0,0 +1,26 @@ +export async function dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const { game } = request.query; + if (game !== 'pong' && game !== 'tetris') { + return reply.code(400).send({ error: "Specified game does not exist" }); + } + deleteMatchHistory.run(game, userId); + if (game === 'pong') { + deleteStatsPong.run(userId); + } + else if (game === 'tetris') { + deleteStatsTetris.run(userId); + } + return reply.code(200).send({ msg: "Match history deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dMember.js b/src/api/user/dMember.js new file mode 100644 index 0000000..32630b9 --- /dev/null +++ b/src/api/user/dMember.js @@ -0,0 +1,22 @@ +export async function dMember(request, reply, fastify, getUserInfo, changeDisplayName) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const user = request.user; + const member = request.params.member; + if (member === 'displayName') { + changeDisplayName.run("", request.params.userId); + return reply.code(200).send({ msg: "Display name deleted successfully" }); + } else { + return reply.code(400).send({ msg: "Member does not exist" }) + } + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dUser.js b/src/api/user/dUser.js new file mode 100644 index 0000000..fad5571 --- /dev/null +++ b/src/api/user/dUser.js @@ -0,0 +1,19 @@ +export async function dUser(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteFriends, deleteUser) { + try { + if (request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + deleteMatchHistory.run('pong', userId); + deleteMatchHistory.run('tetris', userId); + deleteFriends.run(userId); + deleteUser.run(userId); + return reply.code(200).send({ msg: "User deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/default.js b/src/api/user/default.js index a5efe65..88c7a4e 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -1,61 +1,160 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; +import cors from '@fastify/cors' import Database from 'better-sqlite3'; -var env = process.env.NODE_ENV || 'development'; +import { gUsers } from './gUsers.js'; +import { gUser } from './gUser.js'; +import { gNumberUsers } from './gNumberUsers.js'; +import { gFriends } from './gFriends.js'; +import { gNumberFriends } from './gNumberFriends.js'; +import { gMatchHistory } from './gMatchHistory.js'; +import { gNumberMatches } from './gNumberMatches.js'; +import { pUser } from './pUser.js'; +import { pFriend } from './pFriend.js'; +import { pMatchHistory } from './pMatchHistory.js'; +import { uMember } from './uMember.js'; +import { dUser } from './dUser.js'; +import { dMember } from './dMember.js'; +import { dFriends } from './dFriends.js'; +import { dFriend } from './dFriend.js'; +import { dMatchHistory } from './dMatchHistory.js'; +import { pAvatar } from './pAvatar.js'; +import { gAvatar } from './gAvatar.js'; +import { uAvatar } from './uAvatar.js'; +import { dAvatar } from './dAvatar.js'; +import { pPing } from './pPing.js'; +import { gPing } from './gPing.js'; + +const env = process.env.NODE_ENV || 'development'; let database; - if (!env || env === 'development') { - database = new Database(":memory:", { verbose: console.log }); + database = new Database(':memory:', { verbose: console.log }); } else { - var dbPath = process.env.DB_PATH || '/db/db.sqlite' + const dbPath = process.env.DB_PATH || '/db/db.sqlite' database = new Database(dbPath); } function prepareDB() { database.exec(` CREATE TABLE IF NOT EXISTS userData ( - username TEXT PRIMARY KEY, - displayName TEXT + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT, + displayName TEXT, + avatarId INTEGER, + pongWins INTEGER, + pongLosses INTEGER, + tetrisWins INTEGER, + tetrisLosses INTEGER, + UNIQUE(username), + CHECK(pongWins >= 0), + CHECK(pongLosses >= 0), + CHECK(tetrisWins >= 0), + CHECK(tetrisLosses >= 0) ) STRICT `); database.exec(` CREATE TABLE IF NOT EXISTS friends ( + id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, friendName TEXT, UNIQUE(username, friendName), CHECK(username != friendName) - ) + ) STRICT + `); + database.exec(` + CREATE TABLE IF NOT EXISTS matchHistory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + game TEXT, + date INTEGER, + player1 TEXT, + player2 TEXT, + matchId INTEGER, + CHECK(game = 'pong' OR game = 'tetris'), + CHECK(date >= 0), + CHECK(player1 != player2) + ) STRICT + `); + database.exec(` + CREATE TABLE IF NOT EXISTS activityTime ( + username TEXT PRIMARY KEY, + time TEXT + ) STRICT + `); + database.exec(` + CREATE TABLE IF NOT EXISTS images ( + imageId INTEGER PRIMARY KEY AUTOINCREMENT, + fileName TEXT, + mimeType TEXT, + data BLOB + ) STRICT `); } prepareDB(); // POST -const createUser = database.prepare('INSERT INTO userData (username, displayName) VALUES (?, ?);'); +const createUser = database.prepare('INSERT INTO userData (username, displayName, avatarId, pongWins, pongLosses, tetrisWins, tetrisLosses) VALUES (?, ?, -1, 0, 0, 0, 0);'); const addFriend = database.prepare('INSERT INTO friends (username, friendName) VALUES (?, ?);'); +const addMatch = database.prepare('INSERT INTO matchHistory (game, date, player1, player2, matchId) VALUES (?, ?, ?, ?, ?);'); +const incWinsPong = database.prepare('UPDATE userData SET pongWins = pongWins + 1 WHERE username = ?;'); +const incLossesPong = database.prepare('UPDATE userData SET pongLosses = pongLosses + 1 WHERE username = ?'); +const incWinsTetris = database.prepare('UPDATE userData SET tetrisWins = tetrisWins + 1 WHERE username = ?;'); +const incLossesTetris = database.prepare('UPDATE userData SET tetrisLosses = tetrisLosses + 1 WHERE username = ?'); +const setAvatarId = database.prepare('UPDATE userData SET avatarId = ? WHERE username = ?;'); +const postImage = database.prepare('INSERT INTO images (fileName, mimeType, data) VALUES (?, ?, ?);'); +const setActivityTime = database.prepare(` + INSERT INTO activityTime (username, time) + VALUES (?, ?) + ON CONFLICT(username) DO UPDATE SET time = excluded.time; +`); // PATCH const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;'); +const changeAvatarId = database.prepare('UPDATE userData SET avatarId = ? WHERE username = ?;'); // GET -const getUserInfo = database.prepare('SELECT * FROM userData WHERE username = ?;'); -const getUserData = database.prepare('SELECT * FROM userData;'); -const getFriends = database.prepare('SELECT friendName FROM friends WHERE username = ?;'); -// const isFriend = database.prepare('SELECT 1 FROM friends WHERE username = ? AND friendName = ?;'); +const getUserData = database.prepare('SELECT username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses FROM userData LIMIT ? OFFSET ?;'); +const getUserInfo = database.prepare('SELECT username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses FROM userData WHERE username = ?;'); +const getFriends = database.prepare('SELECT friendName FROM friends WHERE username = ? LIMIT ? OFFSET ?;'); +const getFriend = database.prepare('SELECT friendName FROM friends WHERE username = ? AND friendName = ?;'); +const getMatchHistory = database.prepare('SELECT matchId, date FROM matchHistory WHERE game = ? AND ? IN (player1, player2) LIMIT ? OFFSET ?;'); +const getNumberUsers = database.prepare('SELECT COUNT (DISTINCT username) AS n_users FROM userData;'); +const getNumberFriends = database.prepare('SELECT COUNT (DISTINCT friendName) AS n_friends FROM friends WHERE username = ?;'); +const getNumberMatches = database.prepare('SELECT COUNT (DISTINCT id) AS n_matches FROM matchHistory WHERE game = ? AND ? IN (player1, player2);'); +const getAvatarId = database.prepare('SELECT avatarId FROM userData WHERE username = ?;'); +const getImage = database.prepare('SELECT fileName, mimeType, data FROM images WHERE imageId = ?;'); +const getActivityTime = database.prepare('SELECT time FROM activityTime WHERE username = ?;') // DELETE const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;'); const deleteFriend = database.prepare('DELETE FROM friends WHERE username = ? AND friendName = ?;'); const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;'); +const deleteMatchHistory = database.prepare('DELETE FROM matchHistory WHERE game = ? AND ? IN (player1, player2);'); +const deleteStatsPong = database.prepare('UPDATE userData SET pongWins = 0, pongLosses = 0 WHERE username = ?;'); +const deleteStatsTetris = database.prepare('UPDATE userData SET tetrisWins = 0, tetrisLosses = 0 WHERE username = ?;'); +const deleteAvatarId = database.prepare('UPDATE userData SET avatarId = -1 WHERE username = ?;'); +const deleteImage = database.prepare('DELETE FROM images WHERE imageId = ?;'); +const querySchema = { type: 'object', required: ['iStart', 'iEnd'], properties: { iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } }; +const bodySchemaMember = { type: 'object', properties: { displayName: { type: 'string' } } }; +const querySchemaMatchHistory = { type: 'object', required: ['game', 'iStart', 'iEnd'], properties: { game: { type: 'string' }, iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } }; +const bodySchemaMatchHistory = { type: 'object', required: ['game', 'date', 'myScore'], properties: { game: { type: 'string' }, date: { type: 'integer', minimum: 0 }, opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } }; +const querySchemaMatchHistoryGame = { type: 'object', required: ['game'], properties: { game: { type: 'string' } } }; /** - * @param {import('fastify').FastifyInstance} fastify - * @param {import('fastify').FastifyPluginOptions} options + * @param {import('fastify').FastifyInstance} fastify + * @param {import('fastify').FastifyPluginOptions} options */ export default async function(fastify, options) { + + fastify.register(cors, { + origin: process.env.CORS_ORIGIN || 'http://localhost:5173', + credentials: true, + methods: [ "GET", "POST", "PATCH", "DELETE", "OPTIONS" ] + }); + fastify.register(fastifyJWT, { secret: process.env.JWT_SECRET || '123456789101112131415161718192021', cookie: { @@ -63,8 +162,13 @@ export default async function(fastify, options) { }, }); fastify.register(fastifyCookie); + fastify.addContentTypeParser( + ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], + { parseAs: 'buffer' }, + async (request, payload) => payload + ); - fastify.decorate("authenticate", async function(request, reply) { + fastify.decorate('authenticate', async function(request, reply) { try { const jwt = await request.jwtVerify(); request.user = jwt.user; @@ -73,178 +177,89 @@ export default async function(fastify, options) { } }); - fastify.decorate("authenticateAdmin", async function(request, reply) { + fastify.decorate('authenticateAdmin', async function(request, reply) { try { const jwt = await request.jwtVerify(); if (jwt.user !== 'admin') { - throw (""); + throw ('You lack administrator privileges'); } + request.user = jwt.user; } catch (err) { reply.code(401).send({ error: 'Unauthorized' }); } }); // GET - fastify.get('/users', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const users = getUserData.all(); - - return reply.code(200).send({ users }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + fastify.get('/users', { preHandler: [fastify.authenticate], schema: { querystring: querySchema } }, async (request, reply) => { + return gUsers(request, reply, fastify, getUserData); + }); + fastify.get('/users/count', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return gNumberUsers(request, reply, fastify, getNumberUsers); }); fastify.get('/users/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const info = getUserInfo.get(request.params.userId); - - return reply.code(200).send({ info }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + return gUser(request, reply, fastify, getUserInfo); }); - fastify.get('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const userId = request.params.userId; - - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - - if (userId == request.user || request.user == 'admin') { - const friends = getFriends.all(userId); - - if (!friends) { - return reply.code(404).send({ error: "User does not have friends D:" }); - } - return reply.code(200).send({ friends }); - } - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + fastify.get('/users/:userId/friends', { preHandler: [fastify.authenticate], schema: { querystring: querySchema } }, async (request, reply) => { + return gFriends(request, reply, fastify, getUserInfo, getFriends); + }); + fastify.get('/users/:userId/friends/count', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return gNumberFriends(request, reply, fastify, getUserInfo, getNumberFriends); + }); + fastify.get('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { querystring: querySchemaMatchHistory } }, async (request, reply) => { + return gMatchHistory(request, reply, fastify, getUserInfo, getMatchHistory); + }); + fastify.get('/users/:userId/matchHistory/count', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => { + return gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches); + }); + fastify.get('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return gAvatar(request, reply, fastify, getUserInfo, getAvatarId, getImage); + }); + fastify.get('/ping/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return gPing(request, reply, fastify, getActivityTime); }); // POST fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => { - try { - const userId = request.params.userId; - - if (getUserInfo.get(userId)) { - return reply.code(400).send({ error: "User already exist" }); - } - createUser.run(userId, userId); - return reply.code(200).send({ msg: "User created sucessfully" }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } - }) - fastify.post('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const userId = request.params.userId; - if (request.user != 'admin' && request.user != userId) { - return reply.code(401).send({ error: "Unauthorized" }); - } - if (!request.body || !request.body.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - if (!getUserInfo.get(request.body.user)) { - return reply.code(404).send({ error: "Friend does not exist" }); - } - if (request.body.user === userId) { - return reply.code(400).send({ error: "You can't add yourself :D" }); - } - addFriend.run(userId, request.body.user) - return reply.code(200).send({ msg: "Friend added sucessfully" }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + return pUser(request, reply, fastify, getUserInfo, createUser); }); + fastify.post('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return pFriend(request, reply, fastify, getUserInfo, getFriend, addFriend); + }); + fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMatchHistory } }, async (request, reply) => { + return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris); + }); + fastify.post('/users/:userId/avatar', { bodyLimit: 5242880, preHandler: [fastify.authenticate] }, async (request, reply) => { + return pAvatar(request, reply, fastify, getUserInfo, setAvatarId, postImage); + }); + fastify.post('/ping', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return pPing(request, reply, fastify, setActivityTime); + }) // PATCH - fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const userId = request.params.userId; - if (request.user != 'admin' && request.user != userId) { - return reply.code(401).send({ error: "Unauthorized" }); - } - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - const member = request.params.member; - - if (member === 'displayName') { - if (!request.body || !request.body.displayName) { - return reply.code(400).send({ error: "Please specify a displayName" }); - } - - changeDisplayName.run(request.body.displayName, userId); - return reply.code(200).send({ msg: "displayName modified sucessfully" }); - } - return reply.code(400).send({ error: "Member does not exist" }) - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } - }) + fastify.patch('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return uAvatar(request, reply, fastify, getUserInfo, setAvatarId, getAvatarId, deleteAvatarId, postImage, deleteImage); + }); + fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMember } }, async (request, reply) => { + return uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId); + }); // DELETE - /** - * @description Can be used to delete a user from the db - */ fastify.delete('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => { - try { - if (!getUserInfo(request.params.userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - deleteUser.run(request.params.userId); - deleteFriends.run(request.params.userId); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + return dUser(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteFriends, deleteUser); }); fastify.delete('/users/:userId/:member', { preHandler: fastify.authenticate }, async (request, reply) => { - try { - const user = request.user; - const member = request.params.member; - if (user == 'admin' || user == request.params.userId) { - if (member == 'displayName') { - changeDisplayName.run("", request.params.userId); - return reply.code(200).send({ msg: "displayName cleared sucessfully" }); - } - return reply.code(400).send({ msg: "member does not exist" }) - } else { - return reply.code(401).send({ error: 'You dont have the right to delete this' }); - } - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } - + return dMember(request, reply, fastify, getUserInfo, changeDisplayName); + }); + fastify.delete('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return dFriends(request, reply, fastify, getUserInfo, deleteFriends); }); fastify.delete('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const userId = request.params.userId; - const friendId = request.params.friendId; - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - if (request.user != 'admin' && request.user != userId) { - return reply.code(401).send({ error: "Unauthorized" }); - } - deleteFriend.run(userId, friendId); - return reply.code(200).send({ msg: "Friend remove sucessfully" }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + return dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend); + }); + fastify.delete('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => { + return dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris); + }); + fastify.delete('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return dAvatar(request, reply, fastify, getUserInfo, getAvatarId, deleteAvatarId, deleteImage); }); } diff --git a/src/api/user/gAvatar.js b/src/api/user/gAvatar.js new file mode 100644 index 0000000..f7c386f --- /dev/null +++ b/src/api/user/gAvatar.js @@ -0,0 +1,23 @@ +export async function gAvatar(request, reply, fastify, getUserInfo, getAvatarId, getImage) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const imageId = getAvatarId.get(userId); + if (imageId.avatarId === -1) { + return reply.code(404).send({ error: "User does not have an avatar" }); + } + const image = getImage.get(imageId.avatarId); + if (!image) { + return reply.code(404).send({ error: "Avatar does not exist" }); + } + return reply.code(200).type(image.mimeType).send(image.data); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gFriends.js b/src/api/user/gFriends.js new file mode 100644 index 0000000..8050e17 --- /dev/null +++ b/src/api/user/gFriends.js @@ -0,0 +1,29 @@ +export async function gFriends(request, reply, fastify, getUserInfo, getFriends) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const { iStart, iEnd } = request.query; + if (Number(iEnd) < Number(iStart)) { + return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); + } + const friendNames = getFriends.all(userId, Number(iEnd) - Number(iStart), Number(iStart)); + if (!friendNames.length) { + return reply.code(404).send({ error: "No friends exist in the selected range" }); + } + const promises = friendNames.map(async (friendName) => { + const friend = getUserInfo.get(friendName.friendName); + friendName.friendDisplayName = friend.displayName; + return friendName; + }); + const friends = await Promise.all(promises); + return reply.code(200).send({ friends }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gMatchHistory.js b/src/api/user/gMatchHistory.js new file mode 100644 index 0000000..e47517a --- /dev/null +++ b/src/api/user/gMatchHistory.js @@ -0,0 +1,42 @@ +const score_url = process.env.SCORE_URL + +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(`${score_url}/${match.matchId}`, { method: "GET" }); + if (!res.ok) { + throw new Error('Failed to fetch item from blockchain API'); + } + const resJson = await res.json(); + resJson.score.date = match.date; + if (resJson.score.p2 === "" && resJson.score.p2Score === 0) { + delete resJson.score.p2; + delete resJson.score.p2Score; + } + return resJson; + }); + const matchHistory = await Promise.all(promises); + return reply.code(200).send({ matchHistory }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gNumberFriends.js b/src/api/user/gNumberFriends.js new file mode 100644 index 0000000..03730bf --- /dev/null +++ b/src/api/user/gNumberFriends.js @@ -0,0 +1,17 @@ +export async function gNumberFriends(request, reply, fastify, getUserInfo, getNumberFriends) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + + const row = getNumberFriends.get(userId); + return reply.code(200).send({ n_friends: row.n_friends }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gNumberMatches.js b/src/api/user/gNumberMatches.js new file mode 100644 index 0000000..a784fac --- /dev/null +++ b/src/api/user/gNumberMatches.js @@ -0,0 +1,20 @@ +export async function gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const { game } = request.query; + if (game !== 'pong' && game !== 'tetris') { + return reply.code(400).send({ error: "Specified game does not exist" }); + } + const row = getNumberMatches.get(game, userId); + return reply.code(200).send({ n_matches: row.n_matches }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gNumberUsers.js b/src/api/user/gNumberUsers.js new file mode 100644 index 0000000..b8c025d --- /dev/null +++ b/src/api/user/gNumberUsers.js @@ -0,0 +1,9 @@ +export async function gNumberUsers(request, reply, fastify, getNumberUsers) { + try { + const row = getNumberUsers.get(); + return reply.code(200).send({ n_users: row.n_users }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gPing.js b/src/api/user/gPing.js new file mode 100644 index 0000000..b14f003 --- /dev/null +++ b/src/api/user/gPing.js @@ -0,0 +1,28 @@ +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} reply + * @param {import('fastify').FastifyInstance} fastify + */ +export async function gPing(request, reply, fastify, getActivityTime) { + try { + const user = request.params.userId; + const time = getActivityTime.get(user); + + if (!time || !time.time) { + return reply.code(404).send({ error: "User not found or no activity time recorded" }); + } + + const lastSeenTime = new Date(time.time); + const now = new Date(); + const oneMinuteAgo = new Date(now.getTime() - 60000); // 60,000 ms = 1 minute + + const isActiveInLastMinute = lastSeenTime >= oneMinuteAgo; + + return reply.code(200).send({ + isLogged: isActiveInLastMinute + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gUser.js b/src/api/user/gUser.js new file mode 100644 index 0000000..78e9a71 --- /dev/null +++ b/src/api/user/gUser.js @@ -0,0 +1,24 @@ +export async function gUser(request, reply, fastify, getUserInfo) { + try { + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const userInfo = getUserInfo.get(userId); + return reply.code(200).send({ + username: userInfo.username, + displayName: userInfo.displayName, + pong: { + wins: userInfo.pongWins, + losses: userInfo.pongLosses + }, + tetris: { + wins: userInfo.tetrisWins, + losses: userInfo.tetrisLosses + } + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gUsers.js b/src/api/user/gUsers.js new file mode 100644 index 0000000..bf42f33 --- /dev/null +++ b/src/api/user/gUsers.js @@ -0,0 +1,28 @@ +export async function gUsers(request, reply, fastify, getUserData) { + try { + const { iStart, iEnd } = request.query; + if (Number(iEnd) < Number(iStart)) { + return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); + } + const users = getUserData.all(Number(iEnd) - Number(iStart), Number(iStart)); + if (!users.length) { + return reply.code(404).send({ error: "No users exist in the selected range" }); + } + const usersFormat = users.map(obj => ({ + username: obj.username, + displayName: obj.displayName, + pong: { + wins: obj.pongWins, + losses: obj.pongLosses + }, + tetris: { + wins: obj.tetrisWins, + losses: obj.tetrisLosses + } + })); + return reply.code(200).send({ usersFormat }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pAvatar.js b/src/api/user/pAvatar.js new file mode 100644 index 0000000..f90299e --- /dev/null +++ b/src/api/user/pAvatar.js @@ -0,0 +1,45 @@ +import sharp from 'sharp'; + +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} reply + * @param {import('fastify').FastifyInstance} fastify + */ +export async function pAvatar(request, reply, fastify, getUserInfo, setAvatarId, postImage) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + + // Read the raw body as a Buffer + const buffer = request.body; + + if (!buffer) { + return reply.code(400).send({ error: "No file uploaded" }); + } + + // Check file size (5MB limit) + if (buffer.length > 5 * 1024 * 1024) { + return reply.code(400).send({ error: "File too large" }); + } + + // Convert to WebP + const webpBuffer = await sharp(buffer).toFormat('webp').toBuffer(); + + // Save the image and update the user's avatar + const mimeType = request.headers['content-type']; + const fileName = `avatar_${userId}.webp`; + const imageId = postImage.run(fileName, mimeType, webpBuffer); + + setAvatarId.run(imageId.lastInsertRowid, userId); + + return reply.code(200).send({ msg: "Avatar uploaded successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pFriend.js b/src/api/user/pFriend.js new file mode 100644 index 0000000..ae7d414 --- /dev/null +++ b/src/api/user/pFriend.js @@ -0,0 +1,26 @@ +export async function pFriend(request, reply, fastify, getUserInfo, getFriend, addFriend) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const friendId = request.params.friendId; + if (!getUserInfo.get(friendId)) { + return reply.code(404).send({ error: "Friend does not exist" }); + } + if (friendId === userId) { + return reply.code(400).send({ error: "You can't add yourself :D" }); + } + if (getFriend.get(userId, friendId)) { + return reply.code(400).send({ error: "Friend already added" }); + } + addFriend.run(userId, friendId) + return reply.code(200).send({ msg: "Friend added successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pMatchHistory.js b/src/api/user/pMatchHistory.js new file mode 100644 index 0000000..eb60871 --- /dev/null +++ b/src/api/user/pMatchHistory.js @@ -0,0 +1,72 @@ +const score_url = process.env.SCORE_URL || "http://localhost:3003"; + +async function fetchSave(request, reply, userId, addMatch) { + let opponentName = ''; + let opponentScore = 0; + if (request.body.opponent) { + opponentName = request.body.opponent; + } + if (request.body.opponentScore !== undefined) { + opponentScore = request.body.opponentScore; + } else { + opponentScore = 0; + } + const res = await fetch(score_url + "/", { 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) { + 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" }); + } + } + fetchSave(request, reply, userId, addMatch); + if (request.body.game === 'pong') { + if (request.body.myScore > request.body.opponentScore) { + incWinsPong.run(userId); + incLossesPong.run(request.body.opponent); + } else if (request.body.myScore < request.body.opponentScore) { + incWinsPong.run(request.body.opponent); + incLossesPong.run(userId); + } + } + else if (request.body.game === 'tetris' && request.body.opponent && request.body.opponentScore) { + if (request.body.myScore > request.body.opponentScore) { + incWinsTetris.run(userId); + incLossesTetris.run(request.body.opponent); + } else if (request.body.myScore < request.body.opponentScore) { + incWinsTetris.run(request.body.opponent); + incLossesTetris.run(userId); + } + } + return reply.code(200).send({ msg: "Match successfully saved to the blockchain" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pPing.js b/src/api/user/pPing.js new file mode 100644 index 0000000..b7ff9fd --- /dev/null +++ b/src/api/user/pPing.js @@ -0,0 +1,21 @@ +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} request + * @param {import('fastify').Fastify} fastify + */ +export async function pPing(request, reply, fastify, setActivityTime) { + try { + const user = request.user; + const currentTime = new Date().toISOString(); + + setActivityTime.run(user, currentTime); + + return reply.code(200) + .send({ + msg: "last seen time updated successfully" + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pUser.js b/src/api/user/pUser.js new file mode 100644 index 0000000..31ce8bd --- /dev/null +++ b/src/api/user/pUser.js @@ -0,0 +1,19 @@ +export async function pUser(request, reply, fastify, getUserInfo, createUser) { + try { + const userId = request.params.userId; + if (request.user !== 'admin') { + return reply.code(401).send({ error: "Unauthorized" }); + } + if (getUserInfo.get(userId)) { + return reply.code(400).send({ error: "User already exist" }); + } + if (!request.body || !request.body.displayName) { + return reply.code(400).send({ error: "Please specify a display name" }); + } + createUser.run(userId, request.body.displayName); + return reply.code(200).send({ msg: "User created successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/uAvatar.js b/src/api/user/uAvatar.js new file mode 100644 index 0000000..c3df63a --- /dev/null +++ b/src/api/user/uAvatar.js @@ -0,0 +1,34 @@ +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" }); + } + const buffer = request.body; + if (!buffer) { + return reply.code(400).send({ error: "No file uploaded" }); + } + if (buffer.length > 5 * 1024 * 1024) { + return reply.code(400).send({ error: "File too large" }); + } + const webpBuffer = await sharp(buffer).toFormat('webp').toBuffer(); + const mimeType = request.headers['content-type']; + const fileName = `avatar_${userId}.webp`; + const imageId = postImage.run(fileName, 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" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/uMember.js b/src/api/user/uMember.js new file mode 100644 index 0000000..adf5fce --- /dev/null +++ b/src/api/user/uMember.js @@ -0,0 +1,23 @@ +export async function uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId) { + try { + const userId = request.params.userId; + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); + } + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const member = request.params.member; + if (member === 'displayName') { + if (!request.body || !request.body.displayName) { + return reply.code(400).send({ error: "Please specify a displayName" }); + } + changeDisplayName.run(request.body.displayName, userId); + return reply.code(200).send({ msg: "Display name modified successfully" }); + } + return reply.code(400).send({ error: "Member does not exist" }) + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/contract/scoreStore.json b/src/contract/scoreStore.json deleted file mode 100644 index 552e1bc..0000000 --- a/src/contract/scoreStore.json +++ /dev/null @@ -1 +0,0 @@ -[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"string","name":"p1","type":"string"},{"internalType":"string","name":"p2","type":"string"},{"internalType":"uint128","name":"p1Score","type":"uint128"},{"internalType":"uint128","name":"p2Score","type":"uint128"}],"name":"addScore","outputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"id","type":"uint256"}],"name":"getScore","outputs":[{"components":[{"internalType":"string","name":"p1","type":"string"},{"internalType":"string","name":"p2","type":"string"},{"internalType":"uint128","name":"p1Score","type":"uint128"},{"internalType":"uint128","name":"p2Score","type":"uint128"}],"internalType":"struct score","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"scores","outputs":[{"internalType":"string","name":"p1","type":"string"},{"internalType":"string","name":"p2","type":"string"},{"internalType":"uint128","name":"p1Score","type":"uint128"},{"internalType":"uint128","name":"p2Score","type":"uint128"}],"stateMutability":"view","type":"function"}] diff --git a/src/front/index.html b/src/front/index.html index f2f5a6a..dbd12eb 100644 --- a/src/front/index.html +++ b/src/front/index.html @@ -1,19 +1,57 @@ - - - - Vite + Tailwind Test - - - -
-

Vite + Tailwind

-

🚀 Looks like it's working!

- -
- - + + + + + Vite + Tailwind Test + + + + + + + + + + +
+
+ +
+
+ +
+ +
+ + + + diff --git a/src/front/static/assets/favicon.ico b/src/front/static/assets/favicon.ico new file mode 100644 index 0000000..18945be Binary files /dev/null and b/src/front/static/assets/favicon.ico differ diff --git a/src/front/static/assets/fonts/Kubasta.otf b/src/front/static/assets/fonts/Kubasta.otf new file mode 100644 index 0000000..06001b2 Binary files /dev/null and b/src/front/static/assets/fonts/Kubasta.otf differ diff --git a/src/front/static/assets/fonts/Kubasta.ttf b/src/front/static/assets/fonts/Kubasta.ttf new file mode 100644 index 0000000..18f5b82 Binary files /dev/null and b/src/front/static/assets/fonts/Kubasta.ttf differ diff --git a/src/front/static/assets/pong.svg b/src/front/static/assets/pong.svg new file mode 100644 index 0000000..6da6f5c --- /dev/null +++ b/src/front/static/assets/pong.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/front/static/assets/tetrio.svg b/src/front/static/assets/tetrio.svg new file mode 100644 index 0000000..7965017 --- /dev/null +++ b/src/front/static/assets/tetrio.svg @@ -0,0 +1,114 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/src/front/static/css/style.css b/src/front/static/css/style.css new file mode 100644 index 0000000..65bae17 --- /dev/null +++ b/src/front/static/css/style.css @@ -0,0 +1,88 @@ +@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@100..900&display=swap'); +@import "tailwindcss"; +@layer utilities { + .no-scrollbar::-webkit-scrollbar { + display: none; + } + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } +} + +@font-face { + font-family: Kubasta; + src: url('../assets/fonts/Kubasta.otf') format("opentype"); +} + +@source inline("space-y-{18,46,102,214,438,886,1782,3574,7158,14326,28662,57334,114678,229366,458742,917494}"); +@source inline("mt-{28,56,84,112}"); + +@theme { + --shadow-2x1: 2px 2px 0px black; + /* + --color-kanel-700: #ac5c24; + */ +} + +.default-border { + @apply border-2 + border-t-neutral-100 border-l-neutral-100 border-r-neutral-400 border-b-neutral-400 + dark:border-t-neutral-500 dark:border-l-neutral-500 dark:border-r-neutral-700 dark:border-b-neutral-700 + ; +} + +.reverse-border { + @apply border-2 + border-t-neutral-400 border-l-neutral-400 border-r-neutral-100 border-b-neutral-100 + dark:border-t-neutral-700 dark:border-l-neutral-700 dark:border-r-neutral-500 dark:border-b-neutral-500 + ; +} + +.input-border { + @apply border-2 + border-t-neutral-950 border-l-neutral-950 border-r-neutral-200 border-b-neutral-200 + dark:border-t-neutral-950 dark:border-l-neutral-950 dark:border-r-neutral-600 dark:border-b-neutral-600 + ; +} + +.default-button { + @apply shadow-2x1 + bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-700 + text-neutral-900 dark:text-white + px-4 py-2 + delay-0 duration-150 transition-colors + + border-2 border-t-neutral-100 dark:border-t-neutral-500 border-l-neutral-100 dark:border-l-neutral-500 border-r-neutral-400 dark:border-r-neutral-700 border-b-neutral-400 dark:border-b-neutral-700 + active:border-t-neutral-400 dark:active:border-t-neutral-700 active:border-l-neutral-400 dark:active:border-l-neutral-700 active:border-r-neutral-100 dark:active:border-r-neutral-500 active:border-b-neutral-100 dark:active:border-b-neutral-500 + ; +} + +.taskbar-button { + @apply shadow-2x1 + bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-700 + text-neutral-900 dark:text-white + px-4 py-0.5 + content-center text-center + delay-0 duration-150 transition-colors + + border-2 border-t-neutral-100 dark:border-t-neutral-500 border-l-neutral-100 dark:border-l-neutral-500 border-r-neutral-400 dark:border-r-neutral-700 border-b-neutral-400 dark:border-b-neutral-700 + active:border-t-neutral-400 dark:active:border-t-neutral-700 active:border-l-neutral-400 dark:active:border-l-neutral-700 active:border-r-neutral-100 dark:active:border-r-neutral-500 active:border-b-neutral-100 dark:active:border-b-neutral-500 + ; +} + +.menu-default-button { + @apply w-46 h-12 + text-neutral-900 dark:text-white + bg-neutral-200 hover:bg-neutral-300 + dark:bg-neutral-800 dark:hover:bg-neutral-700 + ; +} + +.menu-default-label { + @apply w-46 h-12 + text-neutral-900 dark:text-white + bg-neutral-200 + dark:bg-neutral-800 + ; +} diff --git a/src/front/static/ts/main.ts b/src/front/static/ts/main.ts new file mode 100644 index 0000000..5fee606 --- /dev/null +++ b/src/front/static/ts/main.ts @@ -0,0 +1,174 @@ +import { oneko } from "./oneko.ts"; +import ProfileMenu from "./views/ProfileMenu.ts"; +import FriendsMenu from "./views/Friends.ts"; +let profile_view = new ProfileMenu; +let friends_view = new FriendsMenu; + +export const user_api = import.meta.env.VITE_USER_URL as String; +export const auth_api = import.meta.env.VITE_AUTH_URL as String; + +export async function isLogged(): Promise { + let uuid_req = await fetch(`${auth_api}/me`, { + method: "GET", + credentials: "include", + }); + if (uuid_req.status === 200) { + let uuid = await uuid_req.json(); + document.cookie = `uuid=${uuid.user};max-age=${60 * 60 * 24 * 7}`; + if (!document.getElementById("friends-btn")) + { + const btn: HTMLButtonElement = document.createElement("button") as HTMLButtonElement; + btn.id = "friends-btn"; + btn?.classList.add("taskbar-button"); + btn.innerText = "friends"; + document.getElementById("taskbar-trail")?.prepend(btn); + } + return true; + } + else // 401 + { + document.cookie = `uuid=;max-age=0`; + const btn = document.getElementById("friends-btn") as HTMLButtonElement; + if (btn) btn.remove(); + return false; + } +} + +export const navigationManager = url => { + history.pushState(null, null, url); + router(); +}; + +let view; + +const routes = [ + { path: "/", view: () => import("./views/MainMenu.ts") }, + + { path: "/pong", view: () => import("./views/PongMenu.ts") }, + { path: "/pong/local", view: () => import("./views/Pong.ts") }, + { path: "/pong/tournament", view: () => import("./views/TournamentMenu.ts") }, + + { path: "/tetris", view: () => import("./views/TetrisMenu.ts") }, + { path: "/tetris/solo", view: () => import("./views/Tetris.ts") }, + { path: "/tetris/versus", view: () => import("./views/TetrisVersus.ts") }, + + { path: "/login", view: () => import("./views/LoginPage.ts") }, + { path: "/register", view: () => import("./views/RegisterPage.ts") }, + + { path: "/profile", view: () => import("./views/Profile.ts") }, + { path: "/settings", view: () => import("./views/Settings.ts") }, +]; + +const router = async () => { + + const routesMap = routes.map(route => { + return { route: route, isMatch: location.pathname === route.path }; + }); + + let match = routesMap.find(routeMap => routeMap.isMatch); + + if (!match) + match = { route: routes[0], isMatch: true }; + + if (view) + view.running = false; + + //console.log(match); + + const module = await match.route.view(); + view = new module.default(); + + document.querySelector("#app").innerHTML = await view.getHTML(); + view.run(); +}; + +document.getElementById("profile-button")?.addEventListener("click", () => { profile_view.run(); }); +window.addEventListener("popstate", router); + +document.addEventListener("DOMContentLoaded", () => { + isLogged(); + + document.body.addEventListener("click", e => { + if (!e.target.closest("#taskbar-menu") && !e.target.matches("#profile-button")) { + profile_view.open = false; + document.getElementById("taskbar-menu").innerHTML = ""; + } + if (e.target.matches("#friends-btn")) { + friends_view.open = !friends_view.open; + friends_view.run(); + } + if (e.target.matches("[data-link]")) { + e.preventDefault(); + navigationManager(e.target.href); + } + if (e.target.closest("[data-icon]")) + e.preventDefault(); + }); + + document.body.addEventListener("dblclick", e => { + if (e.target.closest("[data-icon]")) { + e.preventDefault(); + navigationManager(e.target.closest("[data-icon]").href); + } + }); + + router(); +}); + +function updateClock() +{ + const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; + const clock = document.getElementById("taskbar-clock"); + const now = new Date(); + let hours = now.getHours(); + let minutes = now.getMinutes(); + + hours = hours < 10 ? "0" + hours : hours; + minutes = minutes < 10 ? "0" + minutes : minutes; + + clock.innerHTML = `${days[now.getDay()]} ${now.getDate()} ${months[now.getMonth()]} ` + hours + ":" + minutes; +} + +async function pingClock() { + if (await isLogged()) { + fetch(`${user_api}/ping`, { + method: "POST", + credentials: "include" + }); + } +} + +setInterval(updateClock, 5000); +updateClock(); + +setInterval(pingClock, 30000); + +oneko(); + +async function startMenuPP() { + const profileButton = document.getElementById("start-img") as HTMLImageElement; + try { + if(document.cookie.match(new RegExp('(^| )' + "token" + '=([^;]+)'))) { + throw "not today, thank you"; + } + let uuid: String; + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + + + const a = await fetch(`${user_api}/users/${uuid}/avatar`, { + method: "GET", + credentials: "include" + }); + + profileButton.src = a.status === 200 + ? `${user_api}/users/${uuid}/avatar?t=${Date.now()}` + : "https://api.kanel.ovh/pp"; + } catch (err){ + // console.log("not yet logged, going default for start icon..."); + profileButton.src = "https://api.kanel.ovh/id?id=65"; + } +} + +setInterval(startMenuPP, 5000); +startMenuPP(); diff --git a/src/front/static/ts/oneko.ts b/src/front/static/ts/oneko.ts new file mode 100644 index 0000000..01d3453 --- /dev/null +++ b/src/front/static/ts/oneko.ts @@ -0,0 +1,284 @@ +// oneko.js: https://github.com/adryd325/oneko.js +// edited by yosyo specificely for knl_meowscendence. + +let oneko_state: number = 0; // 0 = normal, 1 = pong, 2 = tetris +let mousePosX: number = 0; +let mousePosY: number = 0; +let offsetX: number = 0; +let offsetY: number = 0; + +export function setOnekoState(state: string) { + switch (state) { + case "pong": + oneko_state = 1; + break; + case "tetris": + oneko_state = 2; + break; + default: + oneko_state = 0; + } + return; +} + +export function setOnekoOffset() { + if (oneko_state === 1) + { + offsetX = document.getElementById("window").offsetLeft + 44; + offsetY = document.getElementById("window").offsetTop + 44 + 24; + } + return; +} + +export function setSleepPos() { + mousePosX = document.getElementById("window")?.offsetLeft + 120; + mousePosY = document.getElementById("window")?.offsetTop + 400; +} + +export function setBallPos(x: number, y: number) +{ + mousePosX = x + offsetX; + mousePosY = y + offsetY; + return; +} + +export function oneko() { + const isReducedMotion = + window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || + window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true; + + if (isReducedMotion) return ; + + const nekoEl = document.createElement("div"); + + let nekoPosX = 256; + let nekoPosY = 256; + + let frameCount = 0; + let idleTime = 0; + let idleAnimation = null; + let idleAnimationFrame = 0; + + const nekoSpeed = 10; + const spriteSets = { + idle: [[-3, -3]], + alert: [[-7, -3]], + scratchSelf: [ + [-5, 0], + [-6, 0], + [-7, 0], + ], + scratchWallN: [ + [0, 0], + [0, -1], + ], + scratchWallS: [ + [-7, -1], + [-6, -2], + ], + scratchWallE: [ + [-2, -2], + [-2, -3], + ], + scratchWallW: [ + [-4, 0], + [-4, -1], + ], + tired: [[-3, -2]], + sleeping: [ + [-2, 0], + [-2, -1], + ], + N: [ + [-1, -2], + [-1, -3], + ], + NE: [ + [0, -2], + [0, -3], + ], + E: [ + [-3, 0], + [-3, -1], + ], + SE: [ + [-5, -1], + [-5, -2], + ], + S: [ + [-6, -3], + [-7, -2], + ], + SW: [ + [-5, -3], + [-6, -1], + ], + W: [ + [-4, -2], + [-4, -3], + ], + NW: [ + [-1, 0], + [-1, -1], + ], + }; + + function init() { + nekoEl.id = "oneko"; + nekoEl.ariaHidden = true; + nekoEl.style.width = "32px"; + nekoEl.style.height = "32px"; + nekoEl.style.position = "fixed"; + nekoEl.style.pointerEvents = "none"; + nekoEl.style.imageRendering = "pixelated"; + nekoEl.style.left = `${nekoPosX - 16}px`; + nekoEl.style.top = `${nekoPosY - 16}px`; + nekoEl.style.zIndex = 2147483647; + + let nekoFile = "https://kanel.ovh/assets/oneko.gif" + const curScript = document.currentScript + if (curScript && curScript.dataset.cat) { + nekoFile = curScript.dataset.cat + } + nekoEl.style.backgroundImage = `url(${nekoFile})`; + + document.body.appendChild(nekoEl); + + document.addEventListener("mousemove", function (event) { + if (oneko_state == 0) + { + mousePosX = event.clientX; + mousePosY = event.clientY; + } + }); + + window.requestAnimationFrame(onAnimationFrame); + } + + let lastFrameTimestamp: number; + + function onAnimationFrame(timestamp: number) { + // Stops execution if the neko element is removed from DOM + if (!nekoEl.isConnected) { + return; + } + if (!lastFrameTimestamp) { + lastFrameTimestamp = timestamp; + } + if (timestamp - lastFrameTimestamp > 100) { + lastFrameTimestamp = timestamp + frame() + } + window.requestAnimationFrame(onAnimationFrame); + } + + function setSprite(name, frame) { + const sprite = spriteSets[name][frame % spriteSets[name].length]; + nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`; + } + + function resetIdleAnimation() { + idleAnimation = null; + idleAnimationFrame = 0; + } + + function idle() { + idleTime += 1; + + // every ~ 20 seconds + if (oneko_state === 2) { + idleAnimation = "sleeping"; + } + else if ( + idleTime > 10 && + Math.floor(Math.random() * 200) == 0 && + idleAnimation == null + ) { + let avalibleIdleAnimations = ["sleeping", "scratchSelf"]; + if (nekoPosX < 32) { + avalibleIdleAnimations.push("scratchWallW"); + } + if (nekoPosY < 32) { + avalibleIdleAnimations.push("scratchWallN"); + } + if (nekoPosX > window.innerWidth - 32) { + avalibleIdleAnimations.push("scratchWallE"); + } + if (nekoPosY > window.innerHeight - 32) { + avalibleIdleAnimations.push("scratchWallS"); + } + idleAnimation = + avalibleIdleAnimations[ + Math.floor(Math.random() * avalibleIdleAnimations.length) + ]; + } + + switch (idleAnimation) { + case "sleeping": + if (idleAnimationFrame < 8) { + setSprite("tired", 0); + break; + } + setSprite("sleeping", Math.floor(idleAnimationFrame / 4)); + if (idleAnimationFrame > 192) { + resetIdleAnimation(); + } + break; + case "scratchWallN": + case "scratchWallS": + case "scratchWallE": + case "scratchWallW": + case "scratchSelf": + setSprite(idleAnimation, idleAnimationFrame); + if (idleAnimationFrame > 9) { + resetIdleAnimation(); + } + break; + default: + setSprite("idle", 0); + return; + } + idleAnimationFrame += 1; + } + + function frame() { + frameCount += 1; + const diffX = nekoPosX - mousePosX; + const diffY = nekoPosY - mousePosY; + const distance = Math.sqrt(diffX ** 2 + diffY ** 2); + + if (distance < nekoSpeed || distance < 48) { + idle(); + return; + } + + idleAnimation = null; + idleAnimationFrame = 0; + + if (idleTime > 1) { + setSprite("alert", 0); + // count down after being alerted before moving + idleTime = Math.min(idleTime, 7); + idleTime -= 1; + return; + } + + let direction; + direction = diffY / distance > 0.5 ? "N" : ""; + direction += diffY / distance < -0.5 ? "S" : ""; + direction += diffX / distance > 0.5 ? "W" : ""; + direction += diffX / distance < -0.5 ? "E" : ""; + setSprite(direction, frameCount); + + nekoPosX -= (diffX / distance) * nekoSpeed; + nekoPosY -= (diffY / distance) * nekoSpeed; + + nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16); + nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16); + + nekoEl.style.left = `${nekoPosX - 16}px`; + nekoEl.style.top = `${nekoPosY - 16}px`; + } + + init(); +} diff --git a/src/front/static/ts/views/Aview.ts b/src/front/static/ts/views/Aview.ts new file mode 100644 index 0000000..810d3ab --- /dev/null +++ b/src/front/static/ts/views/Aview.ts @@ -0,0 +1,10 @@ +export default class { + contructor() + { + } + + setTitle(title) { document.title = title; } + + async getHTML() { return ""; } + async run() { } +}; diff --git a/src/front/static/ts/views/Friends.ts b/src/front/static/ts/views/Friends.ts new file mode 100644 index 0000000..1819f95 --- /dev/null +++ b/src/front/static/ts/views/Friends.ts @@ -0,0 +1,200 @@ +import Aview from "./Aview.ts" +import { dragElement } from "./drag.ts"; +import { setOnekoState } from "../oneko.ts" +import { isLogged, navigationManager, user_api, auth_api } from "../main.ts" + +export default class extends Aview { + + open: Boolean = false; + + constructor() { + super(); + this.setTitle("friends list"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ friends.ts +
+ +
+
+ + + + +
+ + +
+ +
+
+
+ `; + } + + async run() { + if (!await isLogged()) + navigationManager("/"); + + if (this.open === true) { + document.getElementById("friends-menu").innerHTML = await this.getHTML(); + } else { + document.getElementById("friends-menu").innerHTML = ""; + return; + } + + let uuid: String; + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + const friends_error_message = (document.getElementById("friends-error-message") as HTMLParagraphElement); + const friends_list = (document.getElementById("friends_list") as HTMLUListElement); + const new_friend = (document.getElementById("new-friend") as HTMLInputElement); + const add_friend_err = (document.getElementById("add-friend-err") as HTMLParagraphElement); + const add_friend_msg = (document.getElementById("add-friend-msg") as HTMLParagraphElement); + + async function removeFriend(name: String) { + const data_req = await fetch(user_api + "/users/" + uuid + "/friends/" + name, { + method: "DELETE", + credentials: "include", + }); + if (data_req.status === 200) { + console.log("friends removed successfully"); + } else { + console.log("could not remove friend"); + } + list_friends(); + list_friends(); + list_friends(); + list_friends(); + list_friends(); + list_friends(); + } + + async function isFriendLogged(name: string): Promise { + const data_req = await fetch(user_api + "/ping/" + name, { + method: "GET", + credentials: "include", + }); + + if (data_req.status === 404) + return false; + + return (await data_req.json()).isLogged + } + + const list_friends = async () => { + const data_req = await fetch(user_api + "/users/" + uuid + "/friends/count", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + if (data_req.status === 404) { + } + let data = await data_req.json(); + while (friends_list.firstChild) { + friends_list.removeChild(friends_list.firstChild); + } + + if (data.n_friends > 0) { + const list_req = await fetch(user_api + "/users/" + uuid + "/friends?iStart=0&iEnd=50", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + if (list_req.status == 404) { + friends_list.classList.add("hidden") + return; + } else if (list_req.status === 200) { + friends_list.classList.remove("hidden") + + let list = (await list_req.json()).friends as JSON; + + for (let i = 0; i < data.n_friends; i++) { + let new_friends = document.createElement('li'); + + const activitySpan = document.createElement('span'); + const isLogged = await isFriendLogged(list[i].friendName) + activitySpan.textContent = "•"; + if (isLogged == true) + activitySpan.className = "px-0 text-green-500"; + else + activitySpan.className = "px-0 text-red-500"; + + + const span = document.createElement('span'); + span.className = "px-3"; + span.textContent = list[i].friendName; + + const but = document.createElement('button'); + but.textContent = "-"; + but.classList.add("px-0", "py-0", "taskbar-button"); + but.onclick = function() { + removeFriend(list[i].friendName); + }; + + new_friends.appendChild(activitySpan); + new_friends.appendChild(span); + new_friends.appendChild(but); + friends_list.appendChild(new_friends); + } + } else { + friends_error_message.innerHTML = (await list_req.json()).error; + friends_error_message.classList.remove("hidden"); + } + } else { + friends_list.classList.add("hidden") + } + } + + const add_friend = async () => { + const data_req = await fetch(user_api + "/users/" + uuid + "/friends/" + new_friend.value, { + method: "POST", + credentials: "include", + }); + let data = await data_req.json() + if (data_req.status === 200) { + add_friend_msg.innerHTML = data.msg; + add_friend_msg.classList.remove("hidden"); + if (!add_friend_err.classList.contains("hidden")) + add_friend_err.classList.add("hidden") + } else { + add_friend_err.innerHTML = data.error; + add_friend_err.classList.remove("hidden"); + if (!add_friend_msg.classList.contains("hidden")) + add_friend_msg.classList.add("hidden") + list_friends() + return + } + list_friends() + new_friend.value = ''; + } + + try { + const data_req = await fetch(user_api + "/users/" + uuid + "/friends/count", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + if (data_req.status === 200) { + // let data = await data_req.json(); + list_friends() + } + } catch (error) { + friends_error_message.innerHTML = "failed to fetch friends"; + friends_error_message.classList.remove("hidden"); + } + document.getElementById("add-friends-button")?.addEventListener("click", add_friend); + setInterval(list_friends, 30000); + } +} diff --git a/src/front/static/ts/views/LoginPage.ts b/src/front/static/ts/views/LoginPage.ts new file mode 100644 index 0000000..262ebf7 --- /dev/null +++ b/src/front/static/ts/views/LoginPage.ts @@ -0,0 +1,197 @@ +import Aview from "./Aview.ts" +import { dragElement } from "./drag.ts" +import { setOnekoState } from "../oneko.ts" +import { isLogged, navigationManager, user_api, auth_api } from "../main.ts" + +export default class extends Aview { + + constructor() + { + super(); + this.setTitle("login"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ login.ts +
+ + + × +
+
+ +
+
+

welcome back ! please login.

+
+ + +
+ +
+ + + +
+ + +
+ +
+ `; + } + + async run() { + document.getElementById("login-google").href = `${auth_api}/login/google`; + 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(auth_api + "/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.log(error); + } + } + + const login = async () => { + const username = (document.getElementById("username") as HTMLInputElement).value; + const password = (document.getElementById("password") as HTMLInputElement).value; + + try { + const data_req = await fetch(auth_api + "/login", { + method: "POST", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ user: username, password: password }), + }); + + if (data_req.status === 200) + { + isLogged(); + 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) + { + const data = await data_req.json(); + document.getElementById("login-error-message").innerHTML = "error: " + data.error; + document.getElementById("login-error-message").classList.remove("hidden"); + } + else + { + throw new Error("invalid response"); + } + + } + catch (error) + { + console.log(error); + document.getElementById("login-error-message").innerHTML = "error: server error, try again later..."; + document.getElementById("login-error-message").classList.remove("hidden"); + } + }; + + document.getElementById("login-button")?.addEventListener("click", login); + } +} diff --git a/src/front/static/ts/views/MainMenu.ts b/src/front/static/ts/views/MainMenu.ts new file mode 100644 index 0000000..8639808 --- /dev/null +++ b/src/front/static/ts/views/MainMenu.ts @@ -0,0 +1,35 @@ +import Aview from "./Aview.ts" +import { isLogged} from "../main.ts" +import { setOnekoState } from "../oneko.ts" + +export default class extends Aview { + + constructor() + { + super(); + this.setTitle("knl is trans(cendence)"); + setOnekoState("default"); + } + + async getHTML() { + //
+ return ` + + `; + } +} diff --git a/src/front/static/ts/views/Pong.ts b/src/front/static/ts/views/Pong.ts new file mode 100644 index 0000000..bf6da69 --- /dev/null +++ b/src/front/static/ts/views/Pong.ts @@ -0,0 +1,341 @@ +import Aview from "./Aview.ts" +import { isLogged, user_api, auth_api } from "../main.js" +import { dragElement } from "./drag.js" +import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts" + +export default class extends Aview { + + running: boolean; + + constructor() + { + super(); + this.setTitle("pong (local match)"); + this.running = true; + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ pong_game.ts +
+ + + × +
+
+ + +
+
+

enter the users ids/names

+
+ + +
+ +
+ +
+ `; + } + + async run() { + dragElement(document.getElementById("window")); + let uuid: string; + + let start: number = 0; + let elapsed: number; + + let game_playing: boolean = false; + let match_over: boolean = false; + let p1_score: number = 0; + let p2_score: number = 0; + let p1_name: string; + let p2_name: string; + + let p1_displayName: string; + let p2_displayName: string; + + let countdown: number = 3; + let countdownTimer: number = 0; + + let canvas; + let ctx; + + const paddleOffset: number = 15; + const paddleHeight: number = 100; + const paddleWidth: number = 10; + const ballSize: number = 10; + + const paddleSpeed: number = 727 * 0.69; + let leftPaddleY: number; + let rightPaddleY: number; + let ballX: number; + let ballY: number; + let ballSpeed: number = 200; + let ballSpeedX: number = 300; + let ballSpeedY: number = 10; + + const keys: Record = {}; + + document.addEventListener("keydown", e => { keys[e.key] = true; }); + document.addEventListener("keyup", e => { keys[e.key] = false; }); + + function movePaddles() { + if ((keys["w"] || keys["W"]) && leftPaddleY > 0) + leftPaddleY -= paddleSpeed * elapsed; + if ((keys["s"] || keys["S"]) && leftPaddleY < canvas.height - paddleHeight) + leftPaddleY += paddleSpeed * elapsed; + if (keys["ArrowUp"] && rightPaddleY > 0) + rightPaddleY -= paddleSpeed * elapsed; + if (keys["ArrowDown"] && rightPaddleY < canvas.height - paddleHeight) + rightPaddleY += paddleSpeed * elapsed; + } + + function getBounceVelocity(paddleY) { + const speed = ballSpeed; + const paddleCenterY = paddleY + paddleHeight / 2; + + let n = (ballY - paddleCenterY) / (paddleHeight / 2); + n = Math.max(-1, Math.min(1, n)); + let theta = n * ((75 * Math.PI) / 180); + ballSpeedY = ballSpeed * Math.sin(theta); + } + + async function moveBall() { + let length = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY); + let scale = ballSpeed / length; + ballX += (ballSpeedX * scale) * elapsed; + ballY += (ballSpeedY * scale) * elapsed; + + if (ballY <= 0 || ballY >= canvas.height - ballSize) + ballSpeedY *= -1; + + if (ballX <= paddleWidth + paddleOffset && ballX >= paddleOffset && + ballY > leftPaddleY && ballY < leftPaddleY + paddleHeight) + { + ballSpeedX *= -1; + ballX = paddleWidth + paddleOffset; + getBounceVelocity(leftPaddleY); + ballSpeed += 10; + } + + if (ballX >= canvas.width - paddleWidth - ballSize - paddleOffset && ballX <= canvas.width - ballSize - paddleOffset && + ballY > rightPaddleY && ballY < rightPaddleY + paddleHeight) + { + ballSpeedX *= -1; + ballX = canvas.width - paddleWidth - ballSize - paddleOffset; + getBounceVelocity(rightPaddleY); + ballSpeed += 10; + } + + // scoring + if (ballX < 0 || ballX > canvas.width - ballSize) + { + setOnekoState("default"); + game_playing = false; + if (ballX < 0) + p2_score++; + else + p1_score++; + + if (p1_score === 3 || p2_score === 3) + { + console.log(isLogged()); + if (await isLogged()) + { + let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + fetch(user_api + "/users/" + uuid + "/matchHistory?game=pong", { + method: "POST", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ + "game": "pong", + "opponent": p2_name, + "myScore": p1_score, + "opponentScore": p2_score, + "date": Date.now(), + }), + }); + } + match_over = true; + } + else + { + countdown = 3; + countdownTimer = performance.now(); + } + + ballX = canvas.width / 2; + ballY = canvas.height / 2; + ballSpeed = 200; + ballSpeedX = 300 * ((ballSpeedX > 0) ? 1 : -1); + ballSpeedY = 10; + ballSpeedX = -ballSpeedX; + leftPaddleY = canvas.height / 2 - paddleHeight / 2; + rightPaddleY = canvas.height / 2 - paddleHeight / 2; + } + setBallPos(ballX, ballY); + } + + function draw() { + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.strokeStyle = "white"; + ctx.beginPath(); + ctx.setLineDash([5, 10]); + ctx.moveTo(canvas.width / 2, 0); + ctx.lineTo(canvas.width / 2, canvas.height); + ctx.stroke(); + + ctx.fillStyle = "white"; + ctx.fillRect(paddleOffset, leftPaddleY, paddleWidth, paddleHeight); + ctx.fillRect(canvas.width - paddleWidth - paddleOffset, rightPaddleY, paddleWidth, paddleHeight); + + ctx.fillStyle = "white"; + if (game_playing) + ctx.fillRect(ballX, ballY, ballSize, ballSize); + + ctx.font = "24px Kubasta"; + let text_score = `${p1_score} - ${p2_score}`; + ctx.fillText(text_score, canvas.width / 2 - (ctx.measureText(text_score).width / 2), 25); + ctx.fillText(p1_displayName, canvas.width / 4 - (ctx.measureText(p1_name).width / 2), 45); + ctx.fillText(p2_displayName, (canvas.width / 4 * 3) - (ctx.measureText(p2_name).width / 2), 45); + + if (match_over) + { + ctx.font = "32px Kubasta"; + const winner = `${ p1_score > p2_score ? p1_displayName : p2_displayName } won :D`; + ctx.fillText(winner, canvas.width / 2 - (ctx.measureText(winner).width / 2), canvas.height / 2 + 16); + document.getElementById("game-buttons").classList.remove("hidden"); + } + } + + function startCountdown() + { + const now = performance.now(); + if (countdown > 0) + { + if (now - countdownTimer >= 500) + { + countdown--; + countdownTimer = now; + } + ctx.font = "48px Kubasta"; + ctx.fillText(countdown.toString(), canvas.width / 2 - 10, canvas.height / 2 + 24); + } + else if (countdown === 0) + { + ctx.font = "48px Kubasta"; + ctx.fillText("Go!", canvas.width / 2 - 30, canvas.height / 2 + 24); + setTimeout(() => { + game_playing = true; + countdown = -1; + }, 500); + } + } + + const gameLoop = async (timestamp: number) => { + elapsed = (timestamp - start) / 1000; + start = timestamp; + if (game_playing) + { + movePaddles(); + await moveBall(); + } + draw(); + if (!game_playing) + startCountdown(); + if (this.running) + requestAnimationFrame(gameLoop); + }; + + + document.getElementById("game-retry")?.addEventListener("click", () => { + setOnekoState("pong"); + document.getElementById("game-buttons").classList.add("hidden"); + game_playing = false; + match_over = false; + p1_score = 0; + p2_score = 0; + + countdown = 3; + countdownTimer = performance.now(); + }); + let p1_input: HTMLInputElement = document.getElementById("player1") as HTMLInputElement; + let p2_input: HTMLInputElement = document.getElementById("player2") as HTMLInputElement; + + p2_input.value = "player 2"; + if (await isLogged()) + { + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + + p1_input.value = uuid; + p1_input.readOnly = true; + } + else + p1_input.value = "player 1"; + + document.getElementById("game-start")?.addEventListener("click", async () => { + let p1_isvalid = true; + let p2_isvalid = true; + if (await isLogged()) { + const p1_req = await fetch(`${user_api}/users/${p1_input.value}`, { + method: "GET", + credentials: "include", + }); + const p2_req = await fetch(`${user_api}/users/${p2_input.value}`, { + method: "GET", + credentials: "include", + }); + if (p1_req.status != 200) + p1_isvalid = false; + else + p1_displayName = (await p1_req.json()).displayName; + + if (p2_req.status != 200) + p2_isvalid = false; + else + p2_displayName = (await p2_req.json()).displayName; + } + else + p1_isvalid = p2_isvalid = false; + p1_name = p1_input.value; + p2_name = p2_input.value; + if (!p1_isvalid) + p1_displayName = p1_name; + if (!p2_isvalid) + p2_displayName = p2_name; + + p1_displayName = p1_displayName.length > 16 ? p1_displayName.substring(0, 16) + "." : p1_displayName; + p2_displayName = p2_displayName.length > 16 ? p2_displayName.substring(0, 16) + "." : p2_displayName; + document.getElementById("player-inputs").remove(); + + canvas = document.createElement("canvas"); + canvas.id = "gameCanvas"; + canvas.classList.add("reverse-border"); + + document.getElementById("main-div").prepend(canvas); + + ctx = canvas.getContext("2d", {alpha: false}); + ctx.canvas.width = 600; + ctx.canvas.height = 600; + + leftPaddleY = canvas.height / 2 - paddleHeight / 2; + rightPaddleY = canvas.height / 2 - paddleHeight / 2; + ballX = canvas.width / 2; + ballY = canvas.height / 2; + + setOnekoState("pong"); + setOnekoOffset(); + requestAnimationFrame(gameLoop); + }); + } +} diff --git a/src/front/static/ts/views/PongMenu.ts b/src/front/static/ts/views/PongMenu.ts new file mode 100644 index 0000000..e7b3bc0 --- /dev/null +++ b/src/front/static/ts/views/PongMenu.ts @@ -0,0 +1,42 @@ +import Aview from "./Aview.ts" +import { dragElement } from "./drag.js" +import { setOnekoState } from "../oneko.ts" + +export default class extends Aview { + + constructor() + { + super(); + this.setTitle("knl is trans(cendence)"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ pong_game.ts +
+ + + × +
+
+
+

welcome to pong!! Oo

+ +
+
+ `; + } + async run() { + dragElement(document.getElementById("window")); + } +} diff --git a/src/front/static/ts/views/Profile.ts b/src/front/static/ts/views/Profile.ts new file mode 100644 index 0000000..89827ac --- /dev/null +++ b/src/front/static/ts/views/Profile.ts @@ -0,0 +1,224 @@ +import Aview from "./Aview.ts" +import { dragElement } from "./drag.ts"; +import { setOnekoState } from "../oneko.ts" +import { isLogged, navigationManager, user_api, auth_api } from "../main.ts" + + +export default class extends Aview { + + constructor() + { + super(); + this.setTitle("profile"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ profile.ts +
+ + + × +
+
+
+
+
+
+
+
    +
+
    +
+
+
+
+
+ `; + } + + async run() { + if (!await isLogged()) + navigationManager("/"); + + let pc: number = 0; + dragElement(document.getElementById("window")); + let uuid: String; + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + + const userdata_req = await fetch(`${user_api}/users/${uuid}`, { + method: "GET", + credentials: "include", + }); + if (userdata_req.status == 404) { + console.log("invalid user"); + return; + } + let userdata = await userdata_req.json(); + + let matchCount_req = await fetch(`${user_api}/users/${uuid}/matchHistory/count?game=pong`, { + method: "GET", + credentials: "include", + }); + let matchCount = await matchCount_req.json(); + pc += matchCount.n_matches; + + let matches_req = await fetch(`${user_api}/users/${uuid}/matchHistory?game=pong&iStart=0&iEnd=${matchCount.n_matches}`, { + method: "GET", + credentials: "include", + }); + let matches = await matches_req.json(); + + let main = document.getElementById("profile-pong-scorelist"); + if (!main) + return console.log("what"); + + if (matches.matchHistory) { + for (let match of matches.matchHistory) { + const p2_req = await fetch(`${user_api}/users/${match.score.p2}`, { + method: "GET", + credentials: "include", + }); + match.score.p1 = userdata.displayName; + match.score.p2 = (await p2_req.json()).displayName; + const newEntry = document.createElement("li"); + newEntry.classList.add("m-1", "default-button", "bg-neutral-200", "dark:bg-neutral-800", "text-neutral-900", "dark:text-white"); + newEntry.innerHTML = match.score.p1Score > match.score.p2Score ? `${match.score.p1} - winner` : `${match.score.p2} - winner`; + main.insertBefore(newEntry, main.firstChild); + + const popup: HTMLDivElement = document.createElement("div"); + const id: number = Math.floor(Math.random() * 100000000000); + popup.id = `${id}`; + 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 = `${id}-header`; + const title = header.appendChild(document.createElement("span")); + title.classList.add("font-[Kubasta]"); + title.innerText = "score-pong.ts"; + const btn = header.appendChild(document.createElement("button")); + btn.innerText = " × "; + btn.onclick = () => { document.getElementById(`${id}`).remove(); }; + + const popup_content: HTMLSpanElement = popup.appendChild(document.createElement("div")); + 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 date = new Date(match.score.date); + popup_content.appendChild(document.createElement("span")).innerText = `${date.toDateString()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`; + const score = popup_content.appendChild(document.createElement("span")); + score.classList.add(); + score.innerText = `${match.score.p1} : ${match.score.p1Score} - ${match.score.p2Score} : ${match.score.p2}`; + const tx = popup_content.appendChild(document.createElement("a")); + tx.href = `https://testnet.snowscan.xyz/tx/${match.tx}`; + tx.innerText = "transaction proof"; + tx.target = "_blank"; + tx.classList.add("default-button", "items-center", "justify-center", "text-center"); + + newEntry.onclick = () => { document.getElementById("app")?.appendChild(popup); dragElement(document.getElementById(`${id}`)); }; + } + } + + matchCount_req = await fetch(`${user_api}/users/${uuid}/matchHistory/count?game=tetris`, { + method: "GET", + credentials: "include", + }); + matchCount = await matchCount_req.json(); + pc += matchCount.n_matches; + + matches_req = await fetch(`${user_api}/users/${uuid}/matchHistory?game=tetris&iStart=0&iEnd=${matchCount.n_matches}`, { + method: "GET", + credentials: "include", + }); + matches = await matches_req.json(); + + main = document.getElementById("profile-tetris-scorelist"); + if (!main) + return console.log("what"); + + // don't read this shit for you mental health + if (matches.matchHistory) { + for (let match of matches.matchHistory) { + if (match.score.p2 != undefined) + { + const p2_req = await fetch(`${user_api}/users/${match.score.p2}`, { + method: "GET", + credentials: "include", + }); + match.score.p2 = (await p2_req.json()).displayName; + } + match.score.p1 = userdata.displayName; + const newEntry = document.createElement("li"); + newEntry.classList.add("m-1", "default-button", "bg-neutral-200", "dark:bg-neutral-800", "text-neutral-900", "dark:text-white"); + newEntry.innerHTML = match.score.p2 != undefined ? + (match.score.p1Score > match.score.p2Score ? `${match.score.p1} - winner` : `${match.score.p2} - winner`) + : + (`solo game - ${match.score.p1Score}`) + ; + main.insertBefore(newEntry, main.firstChild); + + const popup: HTMLDivElement = document.createElement("div"); + const id: number = Math.floor(Math.random() * 100000000000); + popup.id = `${id}`; + 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 = `${id}-header`; + const title = header.appendChild(document.createElement("span")); + title.classList.add("font-[Kubasta]"); + title.innerText = "score-tetris.ts"; + const btn = header.appendChild(document.createElement("button")); + btn.innerText = " × "; + btn.onclick = () => { document.getElementById(`${id}`).remove(); }; + + const popup_content: HTMLSpanElement = popup.appendChild(document.createElement("div")); + 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 date = new Date(match.score.date); + popup_content.appendChild(document.createElement("span")).innerText = `${date.toDateString()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`; + const score = popup_content.appendChild(document.createElement("span")); + score.classList.add(); + score.innerText = match.score.p2 != undefined ? + (`${match.score.p1} : ${match.score.p1Score} - ${match.score.p2Score} : ${match.score.p2}`) + : + (`${match.score.p1} : ${match.score.p1Score}`) + ; + const tx = popup_content.appendChild(document.createElement("a")); + tx.href = `https://testnet.snowscan.xyz/tx/${match.tx}`; + tx.innerText = "transaction proof"; + tx.target = "_blank"; + tx.classList.add("default-button", "items-center", "justify-center", "text-center"); + + newEntry.onclick = () => { document.getElementById("app")?.appendChild(popup); dragElement(document.getElementById(`${id}`)); }; + } + } + + const profile = document.getElementById("profile-profile"); + if (!profile) return; + + const picture = profile.appendChild(document.createElement("img")); + const a = await fetch(`${user_api}/users/${uuid}/avatar`, { + method: "GET", + credentials: "include", + }); + picture.src = a.status === 200 + ? `${user_api}/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"); + + const nametag = profile.appendChild(document.createElement("div")); + nametag.innerHTML = ` +
Hi ${userdata.displayName} ! :D
+
${uuid}
+ `; + nametag.classList.add("text-neutral-900", "dark:text-white"); + + const winrate = profile.appendChild(document.createElement("div")); + winrate.innerHTML = ` +
total playcount: ${pc}
+
pong winrate: ${ (userdata.pong.wins == 0 && userdata.pong.losses == 0) ? "-" : Math.round(userdata.pong.wins / (userdata.pong.wins + userdata.pong.losses) * 100) + " %" }
+
tetris winrate: ${ (userdata.tetris.wins == 0 && userdata.tetris.losses == 0) ? "-" : Math.round(userdata.tetris.wins / (userdata.tetris.wins + userdata.tetris.losses) * 100) + " %" }
+ `; + winrate.classList.add("text-neutral-900", "dark:text-white", "grow", "content-center"); + } +} diff --git a/src/front/static/ts/views/ProfileMenu.ts b/src/front/static/ts/views/ProfileMenu.ts new file mode 100644 index 0000000..44ae8c8 --- /dev/null +++ b/src/front/static/ts/views/ProfileMenu.ts @@ -0,0 +1,87 @@ +import Aview from "./Aview.ts" +import { isLogged, user_api, auth_api } from "../main.ts" + +export default class extends Aview { + async getHTML() { + return ` +
+
+
+
knl_meowscendence
+
+ +
+
+
+ +
+
+
+ `; + } + + open: boolean = false; + + async run() { + let uuid: String; + if (this.open) + { + this.open = false; + document.getElementById("taskbar-menu").innerHTML = ""; + return ; + } + this.open = true; + document.getElementById("taskbar-menu").innerHTML = await this.getHTML(); + + async function getMainHTML() { + if (!(await isLogged())) + { + document.getElementById("menu-bottom-div")?.classList.add("hidden"); + return ` + login + register + `; + } + document.getElementById("menu-bottom-div")?.classList.remove("hidden"); + + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + const userdata_req = await fetch(`${user_api}/users/${uuid}`, { + method: "GET", + credentials: "include", + }); + if (userdata_req.status == 404) + { + console.log("invalid user"); + return ; + } + let userdata = await userdata_req.json(); + + return ` + hi, ${ userdata.displayName.length > 8 ? userdata.displayName.substring(0, 8) + "." : userdata.displayName } ! +
+ profile + settings + `; + } + + document.getElementById("profile-items").innerHTML = await getMainHTML(); + requestAnimationFrame(() => { + document.getElementById("menu-logout")?.addEventListener("click", async () => { + let req = fetch(`${auth_api}/logout`, { + method: "GET", + credentials: "include", + }); + req.then((res) => { + isLogged(); + if (res.status === 200) + this.run(); + else + console.log("logout failed"); + }); + }); + }); + } +} diff --git a/src/front/static/ts/views/RegisterPage.ts b/src/front/static/ts/views/RegisterPage.ts new file mode 100644 index 0000000..b002d29 --- /dev/null +++ b/src/front/static/ts/views/RegisterPage.ts @@ -0,0 +1,104 @@ +import Aview from "./Aview.ts" +import { dragElement } from "./drag.ts"; +import { setOnekoState } from "../oneko.ts" +import { isLogged, navigationManager, user_api, auth_api } from "../main.ts" + +export default class extends Aview { + + constructor() { + super(); + this.setTitle("register"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ register.ts +
+ + + × +
+
+ +
+
+

welcome ! please register.

+
+ + +
+ +
+ + + +
+ + +
+ +
+ `; + } + + async run() { + document.getElementById("register-google").href = `${auth_api}/register/google`; + dragElement(document.getElementById("window")); + const login = async () => { + const username = (document.getElementById("username") as HTMLInputElement).value; + const password = (document.getElementById("password") as HTMLInputElement).value; + + try { + const data_req = await fetch(auth_api + "/register", { + method: "POST", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ user: username, password: password }), + }); + const data = await data_req.json(); + + if (data_req.status === 200) { + let uuid_req = await fetch(auth_api + "/me", { + method: "GET", + credentials: "include", + }); + let uuid = await uuid_req.json(); + document.cookie = `uuid=${uuid.user};max-ages=${60 * 60 * 24 * 7}`; + isLogged(); + navigationManager("/"); + } + 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")?.classList.remove("hidden"); + } + } + else { + throw new Error("invalid response"); + } + + } + catch (error) { + console.log(error); + if (document.getElementById("login-error-message")) { + document.getElementById("login-error-message").innerHTML = "error: server error, try again later..."; + document.getElementById("login-error-message")?.classList.remove("hidden"); + } + } + }; + + document.getElementById("register-button")?.addEventListener("click", login); + } +} diff --git a/src/front/static/ts/views/Settings.ts b/src/front/static/ts/views/Settings.ts new file mode 100644 index 0000000..6abdf41 --- /dev/null +++ b/src/front/static/ts/views/Settings.ts @@ -0,0 +1,181 @@ +import Aview from "./Aview.ts" +import { dragElement } from "./drag.ts"; +import { setOnekoState } from "../oneko.ts" +import { totpEnablePopup } from "./TotpEnable.ts"; +import { totpVerify } from "../../../../api/auth/totpVerify.js"; +import { isLogged, navigationManager, user_api, auth_api } from "../main.ts" + +export default class extends Aview { + + constructor() { + super(); + this.setTitle("profile"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ settings.ts +
+ + + × +
+
+
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+ `; + } + + async run() { + if (!await isLogged()) + navigationManager("/"); + + dragElement(document.getElementById("window")); + + const isTOTPEnabled = async () => { + const totpVerify_req = await fetch(auth_api + '/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; + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + const userdata_req = await fetch(`${user_api}/users/${uuid}`, { + method: "GET", + credentials: "include", + }); + if (userdata_req.status == 404) { + console.log("invalid user"); + return; + } + let userdata = await userdata_req.json(); + + (document.getElementById("displayName-input") as HTMLInputElement).placeholder = userdata.displayName; + (document.getElementById("displayName-input") as HTMLInputElement).value = userdata.displayName; + + document.getElementById("displayName-button")?.addEventListener("click", async () => { + const changeDisplayName_req = await fetch(`${user_api}/users/${uuid}/displayName`, { + method: "PATCH", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ displayName: (document.getElementById("displayName-input") as HTMLInputElement).value }) + }); + if (changeDisplayName_req.status == 200) { + // idk display success + } + else { + // display error ig, uuuh it's in await changeDisplayName.json().error + } + }); + + document.getElementById("deleteAccount-button")?.addEventListener("click", async () => { + const delete_req = await fetch(auth_api + "/", { + method: "DELETE", + credentials: "include", + }); + + if (delete_req.status == 200) + navigationManager("/"); + else + console.log("xd"); // xd????????????? + }); + + const upload = document.getElementById("upload-file") as HTMLInputElement; + upload.addEventListener("change", () => { + const fileList: FileList | null = upload.files; + if (!fileList) + return console.log("empty"); + if (!fileList[0].type.startsWith("image/")) { + console.log("invalid file"); + return; + } + + document.getElementById("upload-preview")?.classList.remove("hidden"); + const img = document.getElementById("upload-preview-img") as HTMLImageElement; + img.classList.remove("hidden"); + + const reader = new FileReader(); + reader.onload = (e) => { + if (!e.target) + return; + img.src = e.target.result as string; + }; + + reader.readAsDataURL(fileList[0]); + }); + + (document.getElementById("upload-submit") as HTMLButtonElement).onclick = async () => { + const up_req = await fetch(`${user_api}/users/${uuid}/avatar`, { + method: "POST", + headers: { "Content-Type": upload.files[0].type } , + credentials: "include", + body: upload.files[0], //upload uuuh whatever i have to upload + }); + 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(`${auth_api}/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(`${auth_api}/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") + } + }); + } + } +} diff --git a/src/front/static/ts/views/Tetris.ts b/src/front/static/ts/views/Tetris.ts new file mode 100644 index 0000000..360961a --- /dev/null +++ b/src/front/static/ts/views/Tetris.ts @@ -0,0 +1,885 @@ +import Aview from "./Aview.ts"; +import { dragElement } from "./drag.js"; +import { setOnekoState, setBallPos, setOnekoOffset, setSleepPos } from "../oneko.ts"; +import { isLogged , user_api, auth_api } from "../main.js"; + +export default class extends Aview { + running: boolean; + + constructor() { + super(); + this.setTitle("tetris (local match)"); + setOnekoState("tetris"); + this.running = true; + } + + async getHTML() { + return ` +
+
+ tetris_game.ts +
+ + + × +
+
+ + +
+
+ + + +
+ +
+
+ `; + } + + async run() { + setSleepPos(); + dragElement(document.getElementById("window")); + const COLS = 10; + const ROWS = 20; + const BLOCK = 30; // pixels per block + + const view = this; + + type Cell = number; // 0 empty, >0 occupied (color index) + + // Tetromino definitions: each piece is an array of rotations, each rotation is a 2D matrix + const TETROMINOES: { [key: string]: number[][][] } = { + I: [ + [ + [0, 0, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 0], + [0, 0, 0, 0], + ], + + [ + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 1, 0], + ], + + [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 0], + ], + + [ + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + ], + ], + J: [ + [ + [2, 0, 0], + [2, 2, 2], + [0, 0, 0], + ], + + [ + [0, 2, 2], + [0, 2, 0], + [0, 2, 0], + ], + + [ + [0, 0, 0], + [2, 2, 2], + [0, 0, 2], + ], + + [ + [0, 2, 0], + [0, 2, 0], + [2, 2, 0], + ], + ], + L: [ + [ + [0, 0, 3], + [3, 3, 3], + [0, 0, 0], + ], + + [ + [0, 3, 0], + [0, 3, 0], + [0, 3, 3], + ], + + [ + [0, 0, 0], + [3, 3, 3], + [3, 0, 0], + ], + + [ + [3, 3, 0], + [0, 3, 0], + [0, 3, 0], + ], + ], + O: [ + [ + [4, 4], + [4, 4], + ], + ], + S: [ + [ + [0, 5, 5], + [5, 5, 0], + [0, 0, 0], + ], + + [ + [0, 5, 0], + [0, 5, 5], + [0, 0, 5], + ], + + [ + [0, 0, 0], + [0, 5, 5], + [5, 5, 0], + ], + + [ + [5, 0, 0], + [5, 5, 0], + [0, 5, 0], + ], + ], + T: [ + [ + [0, 6, 0], + [6, 6, 6], + [0, 0, 0], + ], + + [ + [0, 6, 0], + [0, 6, 6], + [0, 6, 0], + ], + + [ + [0, 0, 0], + [6, 6, 6], + [0, 6, 0], + ], + + [ + [0, 6, 0], + [6, 6, 0], + [0, 6, 0], + ], + ], + Z: [ + [ + [7, 7, 0], + [0, 7, 7], + [0, 0, 0], + ], + + [ + [0, 0, 7], + [0, 7, 7], + [0, 7, 0], + ], + + [ + [0, 0, 0], + [7, 7, 0], + [0, 7, 7], + ], + + [ + [0, 7, 0], + [7, 7, 0], + [7, 0, 0], + ], + ], + }; + + const COLORS = [ + [ "#000000", "#000000" ] , // placeholder for 0 + [ "#00d2e1", "#0080a8" ], // I - cyan + [ "#0092e9", "#001fbf" ], // J - blue + [ "#e79700", "#c75700" ], // L - orange + [ "#d8c800", "#8f7700" ], // O - yellow + [ "#59e000", "#038b00" ], // S - green + [ "#de1fdf", "#870087" ], // T - purple + [ "#f06600", "#c10d07" ], // Z - red + ]; + + class Piece { + shape: number[][]; + rotations: number[][][]; + rotationIndex: number; + x: number; + y: number; + colorIndex: number; + + constructor(public type: string) { + this.rotations = TETROMINOES[type]; + this.rotationIndex = 0; + this.shape = this.rotations[this.rotationIndex]; + this.colorIndex = this.findColorIndex(); + + this.x = Math.floor((COLS - this.shape[0].length) / 2); + this.y = -2; //start on tiles 21 and 22 + } + + findColorIndex() { + for (const row of this.shape) + for (const v of row) + if (v) + return v; + return 1; + } + + rotateCW() { + this.rotationIndex = (this.rotationIndex + 1) % this.rotations.length; + this.shape = this.rotations[this.rotationIndex]; + } + + rotateCCW() { + this.rotationIndex = + (this.rotationIndex - 1 + this.rotations.length) % + this.rotations.length; + this.shape = this.rotations[this.rotationIndex]; + } + + getCells(): { x: number; y: number; val: number }[] { + const cells: { x: number; y: number; val: number }[] = []; + + for (let r = 0; r < this.shape.length; r++) { + for (let c = 0; c < this.shape[r].length; c++) { + const val = this.shape[r][c]; + if (val) cells.push({ x: this.x + c, y: this.y + r, val }); + } + } + return cells; + } + } + + class Game { + board: Cell[][]; + canvas: HTMLCanvasElement | null; + holdCanvas: HTMLCanvasElement | null; + queueCanvas: HTMLCanvasElement | null; + ctx: CanvasRenderingContext2D | null; + holdCtx: CanvasRenderingContext2D | null; + queueCtx: CanvasRenderingContext2D | null; + piece: Piece | null = null; + holdPiece: Piece | null = null; + canHold: boolean = true; + nextQueue: string[] = []; + score: number = 0; + level: number = 1; + lines: number = 0; + dropInterval: number = 1000; + lastDrop: number = 0; + isLocking: boolean = false; + lockRotationCount: number = 0; + lockLastRotationCount: number = 0; + isGameOver: boolean = false; + isPaused: boolean = false; + + constructor(canvasId: string) { + const el = document.getElementById( + canvasId, + ) as HTMLCanvasElement | null; + this.canvas = el; + if (!this.canvas) + throw console.log("no canvas :c"); + this.canvas.width = COLS * BLOCK; + this.canvas.height = ROWS * BLOCK; + const ctx = this.canvas.getContext("2d"); + this.ctx = ctx; + if (!this.ctx) + throw console.log("no ctx D:"); + + this.holdCanvas = document.getElementById("hold") as HTMLCanvasElement; + this.queueCanvas = document.getElementById("queue") as HTMLCanvasElement; + if (!this.holdCanvas || !this.queueCanvas) + throw console.log("no canvas :c"); + this.holdCtx = this.holdCanvas.getContext("2d"); + this.queueCtx = this.queueCanvas.getContext("2d"); + if (!this.holdCtx || !this.queueCtx) + return; + + this.holdCtx.clearRect(0, 0, 200, 200); + this.queueCtx.clearRect(0, 0, 500, 500); + + this.board = this.createEmptyBoard(); + this.fillBag(); + + this.spawnPiece(); + this.registerListeners(); + requestAnimationFrame(this.loop.bind(this)); + } + + createEmptyBoard(): Cell[][] { + const b: Cell[][] = []; + for (let r = 0; r < ROWS; r++) { + const row: Cell[] = new Array(COLS).fill(0); + b.push(row); + } + return b; + } + + fillBag() { + // classic 7-bag randomizer + const pieces = Object.keys(TETROMINOES); + const bag = [...pieces]; + for (let i = bag.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [bag[i], bag[j]] = [bag[j], bag[i]]; + } + this.nextQueue.push(...bag); + } + + hold() { + if (!this.canHold) return; + + [this.piece, this.holdPiece] = [this.holdPiece, this.piece]; + if (!this.piece) this.spawnPiece(); + if (!this.piece) return; + + this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2); + this.piece.y = -2; + this.piece.rotationIndex = 0; + this.piece.shape = this.piece.rotations[this.piece.rotationIndex]; + + this.canHold = false; + this.drawHold(); + } + + spawnPiece() { + this.canHold = true; + if (this.nextQueue.length < 7) this.fillBag(); + const type = this.nextQueue.shift()!; + this.piece = new Piece(type); + if (this.collides(this.piece)) { + this.isGameOver = true; + } + + this.drawHold(); + this.drawQueue(); + } + + collides(piece: Piece): boolean { + for (const cell of piece.getCells()) { + if (cell.y >= ROWS) return true; + if (cell.x < 0 || cell.x >= COLS) return true; + if (cell.y >= 0 && this.board[cell.y][cell.x]) return true; + } + return false; + } + + getGhostOffset(piece: Piece): number { + let y: number = 0; + while (true) { + for (const cell of piece.getCells()) { + if ( + cell.y + y >= ROWS || + (cell.y + y >= 0 && this.board[cell.y + y][cell.x]) + ) + return y - 1; + } + + y++; + } + } + + lockPiece() { + if (!this.piece) return; + this.isLocking = false; + let isValid: boolean = false; + for (const cell of this.piece.getCells()) { + if (cell.y >= 0 && cell.y < ROWS && cell.x >= 0 && cell.x < COLS) + this.board[cell.y][cell.x] = cell.val; + if (cell.y > 0) isValid = true; + } + if (!isValid) this.isGameOver = true; + + this.clearLines(); + this.spawnPiece(); + } + + clearLines() { + let linesCleared = 0; + outer: for (let r = ROWS - 1; r >= 0; r--) { + for (let c = 0; c < COLS; c++) if (!this.board[r][c]) continue outer; + + this.board.splice(r, 1); + this.board.unshift(new Array(COLS).fill(0)); + linesCleared++; + r++; + } + + if (linesCleared > 0) { + this.lines += linesCleared; + const points = [0, 40, 100, 300, 1200]; + this.score += (points[linesCleared] || 0) * this.level; + // level up every 10 lines (Fixed Goal System) + const newLevel = Math.floor(this.lines / 10) + 1; + if (newLevel > this.level) { + this.level = newLevel; + this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 75); + } + } + } + + rotatePiece(dir: "cw" | "ccw") { + if (!this.piece) return; + if (this.isLocking && this.lockRotationCount < 15) + this.lockRotationCount++; + // Try wall kicks + const originalIndex = this.piece.rotationIndex; + if (dir === "cw") this.piece.rotateCW(); + else this.piece.rotateCCW(); + const kicks = [0, -1, 1, -2, 2]; + for (const k of kicks) { + this.piece.x += k; + if (!this.collides(this.piece)) return; + this.piece.x -= k; + } + // no kick, revert + this.piece.rotationIndex = originalIndex; + this.piece.shape = this.piece.rotations[originalIndex]; + } + + movePiece(dx: number, dy: number) { + if (!this.piece) return; + this.piece.x += dx; + this.piece.y += dy; + + if (this.collides(this.piece)) { + this.piece.x -= dx; + this.piece.y -= dy; + return false; + } + return true; + } + + hardDrop() { + if (!this.piece) return; + let dropped = 0; + while (this.movePiece(0, 1)) dropped++; + this.score += dropped * 2; + this.lockPiece(); + } + + softDrop() { + if (!this.piece) return; + if (!this.movePiece(0, 1)) return; + else this.score += 1; + } + + keys: Record = {}; + direction: number = 0; + inputDelay = 200; + inputTimestamp = Date.now(); + move: boolean = false; + + inputManager() { + if (this.move || Date.now() > this.inputTimestamp + this.inputDelay) + { + if (this.keys["ArrowLeft"] && !this.keys["ArrowRight"]) + this.movePiece(-1, 0); + else if (!this.keys["ArrowLeft"] && this.keys["ArrowRight"]) + this.movePiece(1, 0); + else if (this.keys["ArrowLeft"] && this.keys["ArrowRight"]) + this.movePiece(this.direction, 0); + this.move = false; + } + } + + removeListeners() { + window.removeEventListener("keydown", (e) => { + this.keys[e.key] = true; + + if (this.isGameOver) return; + + if (e.key === "p" || e.key === "P" || e.key === "Escape") + this.isPaused = !this.isPaused; + + if (this.isPaused) return; + + if (e.key === "ArrowLeft") + { + this.inputTimestamp = Date.now(); + this.direction = -1;//this.movePiece(-1, 0); + this.move = true; + } + else if (e.key === "ArrowRight") + { + this.inputTimestamp = Date.now(); + this.direction = 1;//this.movePiece(1, 0); + this.move = true; + } + else if (e.key === "ArrowDown") this.softDrop(); + else if (e.code === "Space") { + e.preventDefault(); + this.hardDrop(); + } else if (e.key === "Shift" || e.key === "c" || e.key === "C") { + e.preventDefault(); + this.hold(); + } else if (e.key === "x" || e.key === "X" || e.key === "ArrowUp") { + e.preventDefault(); + this.rotatePiece("cw"); + } else if (e.key === "z" || e.key === "Z" || e.key === "Control") { + e.preventDefault(); + this.rotatePiece("ccw"); + } + }); + + document.removeEventListener("keyup", (e) => { + this.keys[e.key] = false; + }); + } + + registerListeners() { + window.addEventListener("keydown", (e) => { + this.keys[e.key] = true; + + if (this.isGameOver) return; + + if (e.key === "p" || e.key === "P" || e.key === "Escape") + this.isPaused = !this.isPaused; + + if (this.isPaused) return; + + if (e.key === "ArrowLeft") + { + this.inputTimestamp = Date.now(); + this.direction = -1;//this.movePiece(-1, 0); + this.move = true; + } + else if (e.key === "ArrowRight") + { + this.inputTimestamp = Date.now(); + this.direction = 1;//this.movePiece(1, 0); + this.move = true; + } + else if (e.key === "ArrowDown") this.softDrop(); + else if (e.code === "Space") { + //e.preventDefault(); + this.hardDrop(); + } else if (e.key === "Shift" || e.key === "c" || e.key === "C") { + this.hold(); + } else if (e.key === "x" || e.key === "X" || e.key === "ArrowUp") { + //e.preventDefault(); + this.rotatePiece("cw"); + } else if (e.key === "z" || e.key === "Z" || e.key === "Control") { + this.rotatePiece("ccw"); + } + }); + + document.addEventListener("keyup", (e) => { + this.keys[e.key] = false; + }); + } + + async loop(timestamp: number) { + if (!view.running) return this.removeListeners(); + if (!this.lastDrop) this.lastDrop = timestamp; + if (!this.isPaused) + { + this.inputManager(); + if (this.isLocking ? timestamp - this.lastDrop > 500 : timestamp - this.lastDrop > this.dropInterval) + { + if (this.isLocking && this.lockRotationCount == this.lockLastRotationCount) + this.lockPiece(); + this.lockLastRotationCount = this.lockRotationCount; + if (!this.movePiece(0, 1)) + { + if (!this.isLocking) + { + this.lockRotationCount = 0; + this.lockLastRotationCount = 0; + this.isLocking = true; + } + } + else if (this.isLocking) + this.lockRotationCount = 0; + this.lastDrop = timestamp; + } + } + this.draw(); + + if (this.isGameOver) + { + if (await isLogged()) + { + let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + fetch(`${user_api}/users/${uuid}/matchHistory?game=tetris`, { + method: "POST", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ + "game": "tetris", + "myScore": this.score, + "date": Date.now(), + }), + }); + } + document.getElementById("game-buttons")?.classList.remove("hidden"); + return ; + } + requestAnimationFrame(this.loop.bind(this)); + } + + drawGrid() { + const ctx = this.ctx; + if (!ctx || !this.canvas) + return; + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + ctx.strokeStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(14.5% 0 0)" : "oklch(55.6% 0 0)"; + for (let r = 0; r <= ROWS; r++) { + // horizontal lines + ctx.beginPath(); + ctx.moveTo(0, r * BLOCK); + ctx.lineTo(COLS * BLOCK, r * BLOCK); + ctx.stroke(); + } + for (let c = 0; c <= COLS; c++) { + ctx.beginPath(); + ctx.moveTo(c * BLOCK, 0); + ctx.lineTo(c * BLOCK, ROWS * BLOCK); + ctx.stroke(); + } + } + + drawBoard() { + this.drawGrid(); + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + const val = this.board[r][c]; + if (val) this.fillBlock(c, r, COLORS[val], this.ctx); + else this.clearBlock(c, r); + } + } + } + + drawPiece() { + if (!this.piece) return; + + for (const cell of this.piece.getCells()) + if (cell.y >= 0) this.fillBlock(cell.x, cell.y, COLORS[cell.val], this.ctx); + + let offset: number = this.getGhostOffset(this.piece); + for (const cell of this.piece.getCells()) + if (cell.y + offset >= 0 && offset > 0) + this.fillGhostBlock(cell.x, cell.y + offset, COLORS[cell.val]); + } + + drawHold() { + if (!this.holdCtx || !this.holdCanvas) return; + + this.holdCtx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)"; + this.holdCtx.fillRect(0, 0, this.holdCanvas.width, this.holdCanvas.height); + + if (!this.holdPiece) return; + let y: number = 0; + for (const row of this.holdPiece.rotations[0]) { + let x: number = 0; + for (const val of row) { + if (val) + this.fillBlock(x + (4 - this.holdPiece.rotations[0].length)/ 2 + 0.35, y + 0.65, this.canHold ? COLORS[this.holdPiece.findColorIndex()] : ["#8c8c84", "#393934"], this.holdCtx); + x++; + } + y++; + } + } + + drawQueue() { + if (!this.queueCtx || !this.queueCanvas) return ; + + this.queueCtx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)"; + this.queueCtx.fillRect(0, 0, this.queueCanvas.width, this.queueCanvas.height); + let placement: number = 0; + for (const nextPiece of this.nextQueue.slice(0, 5)) { + let y: number = 0; + for (const row of TETROMINOES[nextPiece][0]) { + let x: number = 0; + for (const val of row) { + if (val) + this.fillBlock(x + (4 - TETROMINOES[nextPiece][0].length) / 2 + 0.25, y + 0.5 + placement * 2.69 - (nextPiece ==="I" ? 0.35 : 0), COLORS[["I", "J", "L", "O", "S", "T", "Z"].indexOf(nextPiece) + 1], this.queueCtx); + x++; + } + y++; + } + placement++; + } + } + + adjustColor(hex: string, amount: number): string { + let color = hex.startsWith('#') ? hex.slice(1) : hex; + const num = parseInt(color, 16); + let r = (num >> 16) + amount; + let g = ((num >> 8) & 0x00FF) + amount; + let b = (num & 0x0000FF) + amount; + r = Math.max(Math.min(255, r), 0); + g = Math.max(Math.min(255, g), 0); + b = Math.max(Math.min(255, b), 0); + return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, '0')}`; + } + + fillBlock(x: number, y: number, color: string[], ctx: CanvasRenderingContext2D | null) { + if (!ctx) return; + const grad = ctx.createLinearGradient(x * BLOCK, y * BLOCK, x * BLOCK, y * BLOCK + BLOCK); + grad.addColorStop(0, color[0]); + grad.addColorStop(1, color[1]); + ctx.fillStyle = grad; + ctx.fillRect(Math.round(x * BLOCK) + 4, Math.round(y * BLOCK) + 4, BLOCK - 4, BLOCK - 4); + const X = Math.round(x * BLOCK); + const Y = Math.round(y * BLOCK); + const W = BLOCK; + const H = BLOCK; + const S = 4; + + ctx.lineWidth = S; + ctx.beginPath(); + ctx.strokeStyle = color[0]; + ctx.moveTo(X, Y + S / 2); + ctx.lineTo(X + W, Y + S / 2); + ctx.moveTo(X + S / 2, Y); + ctx.lineTo(X + S / 2, Y + H); + ctx.stroke(); + + ctx.beginPath(); + ctx.strokeStyle = this.adjustColor(color[1], -20); + ctx.moveTo(X, Y + H - S / 2); + ctx.lineTo(X + W, Y + H - S / 2); + ctx.moveTo(X + W - S / 2, Y); + ctx.lineTo(X + W - S / 2, Y + H); + ctx.stroke(); + } + fillGhostBlock(x: number, y: number, color: string[]) { + if (!this.ctx) return; + const ctx = this.ctx; + + const X = x * BLOCK; + const Y = y * BLOCK; + const W = BLOCK; + const H = BLOCK; + const S = 4; + + ctx.lineWidth = S; + ctx.beginPath(); + ctx.strokeStyle = this.adjustColor(color[0], -40); + ctx.moveTo(X, Y + S / 2); + ctx.lineTo(X + W, Y + S / 2); + ctx.moveTo(X + S / 2, Y); + ctx.lineTo(X + S / 2, Y + H); + ctx.stroke(); + + ctx.beginPath(); + ctx.strokeStyle = this.adjustColor(color[1], -60); + ctx.moveTo(X, Y + H - S / 2); + ctx.lineTo(X + W, Y + H - S / 2); + ctx.moveTo(X + W - S / 2, Y); + ctx.lineTo(X + W - S / 2, Y + H); + ctx.stroke(); + //ctx.strokeRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); + } + + clearBlock(x: number, y: number) { + if (!this.ctx) return; + const ctx = this.ctx; + ctx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)"; + ctx.fillRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); + } + + drawHUD() { + if (!this.ctx || !this.canvas) return; + const ctx = this.ctx; + ctx.fillStyle = "rgba(0,0,0,0.6)"; + ctx.fillRect(4, 4, 120, 60); + ctx.fillStyle = "#fff"; + ctx.font = "12px Kubasta"; + ctx.fillText(`score: ${this.score}`, 8, 20); + ctx.fillText(`lines: ${this.lines}`, 8, 36); + ctx.fillText(`level: ${this.level}`, 8, 52); + + if (this.isPaused) { + ctx.fillStyle = "rgba(0,0,0,0.7)"; + ctx.fillRect(0, this.canvas.height / 2 - 24, this.canvas.width, 48); + ctx.fillStyle = "#fff"; + ctx.font = "24px Kubasta"; + ctx.textAlign = "center"; + ctx.fillText( + "paused", + this.canvas.width / 2, + this.canvas.height / 2 + 8, + ); + ctx.textAlign = "start"; + } + + if (this.isGameOver) { + ctx.fillStyle = "rgba(0,0,0,0.7)"; + ctx.fillRect(0, this.canvas.height / 2 - 36, this.canvas.width, 72); + ctx.fillStyle = "#fff"; + ctx.font = "28px Kubasta"; + ctx.textAlign = "center"; + ctx.fillText( + "game over", + this.canvas.width / 2, + this.canvas.height / 2 + 8, + ); + ctx.textAlign = "start"; + } + } + + draw() { + if (!this.ctx || !this.canvas) return; + // clear everything + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + this.ctx.fillStyle = "#000"; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + this.ctx.strokeStyle = "#111"; + for (let r = 0; r <= ROWS; r++) { + this.ctx.beginPath(); + this.ctx.moveTo(0, r * BLOCK); + this.ctx.lineTo(COLS * BLOCK, r * BLOCK); + this.ctx.stroke(); + } + for (let c = 0; c <= COLS; c++) { + this.ctx.beginPath(); + this.ctx.moveTo(c * BLOCK, 0); + this.ctx.lineTo(c * BLOCK, ROWS * BLOCK); + this.ctx.stroke(); + } + + this.drawBoard(); + this.drawPiece(); + this.drawHUD(); + this.drawQueue(); + setSleepPos(); + } + } + + document.getElementById("game-retry")?.addEventListener("click", () => { document.getElementById("game-buttons")?.classList.add("hidden"); const game = new Game("board"); }); + const game = new Game("board"); + } +} diff --git a/src/front/static/ts/views/TetrisMenu.ts b/src/front/static/ts/views/TetrisMenu.ts new file mode 100644 index 0000000..8646f83 --- /dev/null +++ b/src/front/static/ts/views/TetrisMenu.ts @@ -0,0 +1,42 @@ +import Aview from "./Aview.ts" +import { dragElement } from "./drag.js" +import { setOnekoState } from "../oneko.ts" + +export default class extends Aview { + + constructor() + { + super(); + this.setTitle("knl is trans(cendence)"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ tetris_game.ts +
+ + + × +
+
+
+

welcome to tetris! :D

+ +
+
+ `; + } + async run() { + dragElement(document.getElementById("window")); + } +} diff --git a/src/front/static/ts/views/TetrisVersus.ts b/src/front/static/ts/views/TetrisVersus.ts new file mode 100644 index 0000000..38c1584 --- /dev/null +++ b/src/front/static/ts/views/TetrisVersus.ts @@ -0,0 +1,987 @@ +import Aview from "./Aview.ts"; +import { isLogged, user_api, auth_api } from "../main.js"; +import { dragElement } from "./drag.js"; +import { setOnekoState, setBallPos, setOnekoOffset, setSleepPos } from "../oneko.ts"; + +export default class extends Aview { + running: boolean; + + constructor() { + super(); + this.setTitle("tetris (local match)"); + setOnekoState("tetris"); + this.running = true; + } + + async getHTML() { + return ` +
+
+ tetris_game.ts +
+ + + × +
+
+ + +
+
+

enter the users ids/names

+
+ + +
+ +
+ + +
+
+ `; + } + + async run() { + setSleepPos(); + dragElement(document.getElementById("window")); + const COLS = 10; + const ROWS = 20; + const BLOCK = 30; // pixels per block + + let uuid: string; + let game1: Game; + let game2: Game; + let p1_score: number = 0; + let p2_score: number = 0; + let p1_name: string; + let p2_name: string; + let p1_displayName: string; + let p2_displayName: string; + + const view = this; + + type Cell = number; + + // Tetromino definitions: each piece is an array of rotations, each rotation is a 2D matrix + const TETROMINOES: { [key: string]: number[][][] } = { + I: [ + [ + [0, 0, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 0], + [0, 0, 0, 0], + ], + + [ + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 1, 0], + ], + + [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 0], + ], + + [ + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + [0, 1, 0, 0], + ], + ], + J: [ + [ + [2, 0, 0], + [2, 2, 2], + [0, 0, 0], + ], + + [ + [0, 2, 2], + [0, 2, 0], + [0, 2, 0], + ], + + [ + [0, 0, 0], + [2, 2, 2], + [0, 0, 2], + ], + + [ + [0, 2, 0], + [0, 2, 0], + [2, 2, 0], + ], + ], + L: [ + [ + [0, 0, 3], + [3, 3, 3], + [0, 0, 0], + ], + + [ + [0, 3, 0], + [0, 3, 0], + [0, 3, 3], + ], + + [ + [0, 0, 0], + [3, 3, 3], + [3, 0, 0], + ], + + [ + [3, 3, 0], + [0, 3, 0], + [0, 3, 0], + ], + ], + O: [ + [ + [4, 4], + [4, 4], + ], + ], + S: [ + [ + [0, 5, 5], + [5, 5, 0], + [0, 0, 0], + ], + + [ + [0, 5, 0], + [0, 5, 5], + [0, 0, 5], + ], + + [ + [0, 0, 0], + [0, 5, 5], + [5, 5, 0], + ], + + [ + [5, 0, 0], + [5, 5, 0], + [0, 5, 0], + ], + ], + T: [ + [ + [0, 6, 0], + [6, 6, 6], + [0, 0, 0], + ], + + [ + [0, 6, 0], + [0, 6, 6], + [0, 6, 0], + ], + + [ + [0, 0, 0], + [6, 6, 6], + [0, 6, 0], + ], + + [ + [0, 6, 0], + [6, 6, 0], + [0, 6, 0], + ], + ], + Z: [ + [ + [7, 7, 0], + [0, 7, 7], + [0, 0, 0], + ], + + [ + [0, 0, 7], + [0, 7, 7], + [0, 7, 0], + ], + + [ + [0, 0, 0], + [7, 7, 0], + [0, 7, 7], + ], + + [ + [0, 7, 0], + [7, 7, 0], + [7, 0, 0], + ], + ], + }; + + const COLORS = [ + [ "#000000", "#000000" ] , // placeholder for 0 + [ "#00d2e1", "#0080a8" ], // I - cyan + [ "#0092e9", "#001fbf" ], // J - blue + [ "#e79700", "#c75700" ], // L - orange + [ "#d8c800", "#8f7700" ], // O - yellow + [ "#59e000", "#038b00" ], // S - green + [ "#de1fdf", "#870087" ], // T - purple + [ "#f06600", "#c10d07" ], // Z - red + [ "#8c8c84", "#393934" ], // garbage - gray + ]; + + class Piece { + shape: number[][]; + rotations: number[][][]; + rotationIndex: number; + x: number; + y: number; + colorIndex: number; + + constructor(public type: string) { + this.rotations = TETROMINOES[type]; + this.rotationIndex = 0; + this.shape = this.rotations[this.rotationIndex]; + this.colorIndex = this.findColorIndex(); + + this.x = Math.floor((COLS - this.shape[0].length) / 2); + this.y = -2; //start on tiles 21 and 22 + } + + findColorIndex() { + for (const row of this.shape) + for (const v of row) + if (v) + return v; + return 1; + } + + rotateCW() { + this.rotationIndex = (this.rotationIndex + 1) % this.rotations.length; + this.shape = this.rotations[this.rotationIndex]; + } + + rotateCCW() { + this.rotationIndex = + (this.rotationIndex - 1 + this.rotations.length) % + this.rotations.length; + this.shape = this.rotations[this.rotationIndex]; + } + + getCells(): { x: number; y: number; val: number }[] { + const cells: { x: number; y: number; val: number }[] = []; + + for (let r = 0; r < this.shape.length; r++) { + for (let c = 0; c < this.shape[r].length; c++) { + const val = this.shape[r][c]; + if (val) cells.push({ x: this.x + c, y: this.y + r, val }); + } + } + return cells; + } + } + + class Game { + board: Cell[][]; + canvas: HTMLCanvasElement | null; + holdCanvas: HTMLCanvasElement | null; + queueCanvas: HTMLCanvasElement | null; + ctx: CanvasRenderingContext2D | null; + holdCtx: CanvasRenderingContext2D | null; + queueCtx: CanvasRenderingContext2D | null; + piece: Piece | null = null; + holdPiece: Piece | null = null; + canHold: boolean = true; + nextQueue: string[] = []; + score: number = 0; + level: number = 1; + lines: number = 0; + garbage: number = 0; + dropInterval: number = 1000; + lastDrop: number = 0; + isLocking: boolean = false; + lockRotationCount: number = 0; + lockLastRotationCount: number = 0; + isGameOver: boolean = false; + isPaused: boolean = false; + id: number; + + constructor(canvasId: string, id: number) { + this.id = id; + const el = document.getElementById( + canvasId + "-board", + ) as HTMLCanvasElement | null; + this.canvas = el; + if (!this.canvas) + throw console.log("no canvas :c"); + this.canvas.width = COLS * BLOCK; + this.canvas.height = ROWS * BLOCK; + const ctx = this.canvas.getContext("2d"); + this.ctx = ctx; + if (!this.ctx) + throw console.log("no ctx D:"); + + this.holdCanvas = document.getElementById(canvasId + "-hold") as HTMLCanvasElement; + this.queueCanvas = document.getElementById(canvasId + "-queue") as HTMLCanvasElement; + if (!this.holdCanvas || !this.queueCanvas) + throw console.log("no canvas :c"); + this.holdCtx = this.holdCanvas.getContext("2d"); + this.queueCtx = this.queueCanvas.getContext("2d"); + if (!this.holdCtx || !this.queueCtx) + return; + + this.holdCtx.clearRect(0, 0, 200, 200); + this.queueCtx.clearRect(0, 0, 500, 500); + + this.board = this.createEmptyBoard(); + if (id == 0) + this.fillBag(); + else + this.nextQueue = game1.nextQueue; + + this.spawnPiece(); + if (id != 0) + this.piece.type = game1.piece.type; + this.registerListeners(); + requestAnimationFrame(this.loop.bind(this)); + } + + createEmptyBoard(): Cell[][] { + const b: Cell[][] = []; + for (let r = 0; r < ROWS; r++) { + const row: Cell[] = new Array(COLS).fill(0); + b.push(row); + } + return b; + } + + fillBag() { + // classic 7-bag randomizer + const pieces = Object.keys(TETROMINOES); + const bag = [...pieces]; + for (let i = bag.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [bag[i], bag[j]] = [bag[j], bag[i]]; + } + this.nextQueue.push(...bag); + } + + hold() { + if (!this.canHold) return; + + [this.piece, this.holdPiece] = [this.holdPiece, this.piece]; + if (!this.piece) this.spawnPiece(); + if (!this.piece) return; + + this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2); + this.piece.y = -2; + this.piece.rotationIndex = 0; + this.piece.shape = this.piece.rotations[this.piece.rotationIndex]; + + this.canHold = false; + this.drawHold(); + } + + spawnPiece() { + this.canHold = true; + if (this.nextQueue.length < 7) this.fillBag(); + const type = this.nextQueue.shift()!; + this.piece = new Piece(type); + if (this.collides(this.piece)) { + game1.isGameOver = true; + game2.isGameOver = true; + } + + this.drawHold(); + this.drawQueue(); + } + + collides(piece: Piece): boolean { + for (const cell of piece.getCells()) { + if (cell.y >= ROWS) return true; + if (cell.x < 0 || cell.x >= COLS) return true; + if (cell.y >= 0 && this.board[cell.y][cell.x]) return true; + } + return false; + } + + getGhostOffset(piece: Piece): number { + let y: number = 0; + while (true) { + for (const cell of piece.getCells()) { + if ( + cell.y + y >= ROWS || + (cell.y + y >= 0 && this.board[cell.y + y][cell.x]) + ) + return y - 1; + } + + y++; + } + } + + lockPiece() { + if (!this.piece) return; + this.isLocking = false; + let isValid: boolean = false; + for (const cell of this.piece.getCells()) { + if (cell.y >= 0 && cell.y < ROWS && cell.x >= 0 && cell.x < COLS) + this.board[cell.y][cell.x] = cell.val; + if (cell.y > 0) isValid = true; + } + if (!isValid) + { + this.id == 0 ? p2_score++ : p1_score++; + game1.isGameOver = true; + game2.isGameOver = true; + } + + if (this.garbage) + { + const empty_spot = Math.floor(Math.random() * 10); + while (this.garbage) + { + //if () // if anything else than 0 on top, die >:3 + this.board.shift(); + this.board.push(Array(COLS).fill(8)); + this.board[19][empty_spot] = 0; + this.garbage--; + } + } + this.clearLines(); + this.spawnPiece(); + } + + addGarbage(lines: number) { + this.garbage += lines; + } + + clearLines() { + let linesCleared = 0; + outer: for (let r = ROWS - 1; r >= 0; r--) { + for (let c = 0; c < COLS; c++) if (!this.board[r][c]) continue outer; + + this.board.splice(r, 1); + this.board.unshift(new Array(COLS).fill(0)); + linesCleared++; + r++; + } + + if (linesCleared > 0) { + this.lines += linesCleared; + const points = [0, 40, 100, 300, 1200]; + this.score += (points[linesCleared] || 0) * this.level; + // level up every 10 lines (Fixed Goal System) + const newLevel = Math.floor(this.lines / 10) + 1; + if (newLevel > this.level) { + this.level = newLevel; + this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 75); + } + + if (this.garbage) + { + while (linesCleared) + { + this.garbage--; + linesCleared--; + if (!this.garbage) + break; + } + } + if (this.id == 0 && linesCleared) + game2.addGarbage(linesCleared < 4 ? linesCleared - 1 : linesCleared); + else + game1.addGarbage(linesCleared < 4 ? linesCleared - 1 : linesCleared); + } + } + + rotatePiece(dir: "cw" | "ccw") { + if (!this.piece) return; + if (this.isLocking && this.lockRotationCount < 15) + this.lockRotationCount++; + // Try wall kicks + const originalIndex = this.piece.rotationIndex; + if (dir === "cw") this.piece.rotateCW(); + else this.piece.rotateCCW(); + const kicks = [0, -1, 1, -2, 2]; + for (const k of kicks) { + this.piece.x += k; + if (!this.collides(this.piece)) return; + this.piece.x -= k; + } + // no kick, revert + this.piece.rotationIndex = originalIndex; + this.piece.shape = this.piece.rotations[originalIndex]; + } + + movePiece(dx: number, dy: number) { + if (!this.piece) return; + this.piece.x += dx; + this.piece.y += dy; + + if (this.collides(this.piece)) { + this.piece.x -= dx; + this.piece.y -= dy; + return false; + } + return true; + } + + hardDrop() { + if (!this.piece) return; + let dropped = 0; + while (this.movePiece(0, 1)) dropped++; + this.score += dropped * 2; + this.lockPiece(); + } + + softDrop() { + if (!this.piece) return; + if (!this.movePiece(0, 1)) return; + else this.score += 1; + } + + keys: Record = {}; + direction: number = 0; + inputDelay = 200; + inputTimestamp = Date.now(); + move: boolean = false; + + inputManager() { + const left = this.id === 0 ? this.keys["KeyA"] : this.keys["Numpad4"] + const right = this.id === 0 ? this.keys["KeyD"] : this.keys["Numpad6"] + if (this.move || Date.now() > this.inputTimestamp + this.inputDelay) + { + if (left && !right) + this.movePiece(-1, 0); + else if (!left && right) + this.movePiece(1, 0); + else if (left && right) + this.movePiece(this.direction, 0); + this.move = false; + } + + /*if (this.keys["ArrowDown"]) + this.softDrop();*/ + } + + registerListeners() { + window.addEventListener("keydown", (e) => { + this.keys[e.code] = true; + + if (this.isGameOver) return; + + if (e.key === "p" || e.key === "P" || e.key === "Escape") + this.isPaused = !this.isPaused; + + if (this.isPaused) return; + + if (this.id === 0 ? e.code === "KeyA" : e.code === "Numpad4") + { + this.inputTimestamp = Date.now(); + this.direction = -1;//this.movePiece(-1, 0); + this.move = true; + } + else if (this.id === 0 ? e.code === "KeyD" : e.code === "Numpad6") + { + this.inputTimestamp = Date.now(); + this.direction = 1;//this.movePiece(1, 0); + this.move = true; + } + else if (this.id === 0 ? e.code === "KeyS" : e.code === "Numpad5") this.softDrop(); + else if (this.id === 0 ? e.code === "Space" : e.code === "Numpad0") { + //e.preventDefault(); + this.hardDrop(); + } else if (this.id === 0 ? e.code === "ShiftLeft" : e.code === "NumpadEnter") { + //e.preventDefault(); + this.hold(); + } else if (this.id === 0 ? (e.code === "KeyE" || e.code === "KeyW") : (e.code === "Numpad9" || e.code === "Numpad8")) { + //e.preventDefault(); + this.rotatePiece("cw"); + } else if (this.id === 0 ? (e.code === "KeyQ" || e.code === "ControlLeft") : e.code === "Numpad7") { + //e.preventDefault(); + this.rotatePiece("ccw"); + } + }); + + document.addEventListener("keyup", (e) => { + this.keys[e.code] = false; + }); + } + + async loop(timestamp: number) { + if (!view.running) return; + if (!this.lastDrop) this.lastDrop = timestamp; + if (!this.isPaused) + { + this.inputManager(); + if (this.isLocking ? timestamp - this.lastDrop > 500 : timestamp - this.lastDrop > this.dropInterval) + { + if (this.isLocking && this.lockRotationCount == this.lockLastRotationCount) + this.lockPiece(); + this.lockLastRotationCount = this.lockRotationCount; + if (!this.movePiece(0, 1)) + { + if (!this.isLocking) + { + this.lockRotationCount = 0; + this.lockLastRotationCount = 0; + this.isLocking = true; + } + } + else if (this.isLocking) + this.lockRotationCount = 0; + this.lastDrop = timestamp; + } + } + this.draw(); + + if (this.isGameOver) + { + if (p1_score != 3 && p2_score != 3) + { + if (this.id == 0) + { + game1 = new Game("board1", 0); + game2 = new Game("board2", 1); + } + return ; + } + if (await isLogged() && this.id == 0) + { + let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + fetch(`${user_api}/users/${uuid}/matchHistory?game=tetris`, { + method: "POST", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ + "game": "tetris", + "opponent": p2_name, + "myScore": p1_score, + "opponentScore": p2_score, + "date": Date.now(), + }), + }); + } + document.getElementById("game-buttons")?.classList.remove("hidden"); + return ; + } + requestAnimationFrame(this.loop.bind(this)); + } + + drawGrid() { + const ctx = this.ctx; + if (!ctx || !this.canvas) + return; + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + ctx.strokeStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(14.5% 0 0)" : "oklch(55.6% 0 0)"; + for (let r = 0; r <= ROWS; r++) { + // horizontal lines + ctx.beginPath(); + ctx.moveTo(0, r * BLOCK); + ctx.lineTo(COLS * BLOCK, r * BLOCK); + ctx.stroke(); + } + for (let c = 0; c <= COLS; c++) { + ctx.beginPath(); + ctx.moveTo(c * BLOCK, 0); + ctx.lineTo(c * BLOCK, ROWS * BLOCK); + ctx.stroke(); + } + } + + drawBoard() { + this.drawGrid(); + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + const val = this.board[r][c]; + if (val) this.fillBlock(c, r, COLORS[val], this.ctx); + else this.clearBlock(c, r); + } + } + } + + drawPiece() { + if (!this.piece) return; + + for (const cell of this.piece.getCells()) + if (cell.y >= 0) this.fillBlock(cell.x, cell.y, COLORS[cell.val], this.ctx); + + let offset: number = this.getGhostOffset(this.piece); + for (const cell of this.piece.getCells()) + if (cell.y + offset >= 0 && offset > 0) + this.fillGhostBlock(cell.x, cell.y + offset, COLORS[cell.val]); + } + + drawHold() { + if (!this.holdCtx || !this.holdCanvas) return; + + this.holdCtx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)"; + this.holdCtx.fillRect(0, 0, this.holdCanvas.width, this.holdCanvas.height); + + if (!this.holdPiece) return; + let y: number = 0; + for (const row of this.holdPiece.rotations[0]) { + let x: number = 0; + for (const val of row) { + if (val) + this.fillBlock(x + (4 - this.holdPiece.rotations[0].length)/ 2 + 0.35, y + 0.5, this.canHold ? COLORS[this.holdPiece.findColorIndex()] : COLORS[8], this.holdCtx); + x++; + } + y++; + } + } + + drawQueue() { + if (!this.queueCtx || !this.queueCanvas) return ; + + this.queueCtx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)"; + this.queueCtx.fillRect(0, 0, this.queueCanvas.width, this.queueCanvas.height); + let placement: number = 0; + for (const nextPiece of this.nextQueue.slice(0, 5)) { + let y: number = 0; + for (const row of TETROMINOES[nextPiece][0]) { + let x: number = 0; + for (const val of row) { + if (val) + this.fillBlock(x + (4 - TETROMINOES[nextPiece][0].length) / 2 + 0.25, y + 0.5 + placement * 2.69 - (nextPiece ==="I" ? 0.35 : 0), COLORS[["I", "J", "L", "O", "S", "T", "Z"].indexOf(nextPiece) + 1], this.queueCtx); + x++; + } + y++; + } + placement++; + } + } + + adjustColor(hex: string, amount: number): string { + let color = hex.startsWith('#') ? hex.slice(1) : hex; + const num = parseInt(color, 16); + let r = (num >> 16) + amount; + let g = ((num >> 8) & 0x00FF) + amount; + let b = (num & 0x0000FF) + amount; + r = Math.max(Math.min(255, r), 0); + g = Math.max(Math.min(255, g), 0); + b = Math.max(Math.min(255, b), 0); + return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, '0')}`; + } + + fillBlock(x: number, y: number, color: string[], ctx: CanvasRenderingContext2D | null) { + if (!ctx) return; + const grad = ctx.createLinearGradient(x * BLOCK, y * BLOCK, x * BLOCK, y * BLOCK + BLOCK); + grad.addColorStop(0, color[0]); + grad.addColorStop(1, color[1]); + ctx.fillStyle = grad; + ctx.fillRect(Math.round(x * BLOCK) + 4, Math.round(y * BLOCK) + 4, BLOCK - 4, BLOCK - 4); + const X = Math.round(x * BLOCK); + const Y = Math.round(y * BLOCK); + const W = BLOCK; + const H = BLOCK; + const S = 4; + + ctx.lineWidth = S; + ctx.beginPath(); + ctx.strokeStyle = color[0]; + ctx.moveTo(X, Y + S / 2); + ctx.lineTo(X + W, Y + S / 2); + ctx.moveTo(X + S / 2, Y); + ctx.lineTo(X + S / 2, Y + H); + ctx.stroke(); + + ctx.beginPath(); + ctx.strokeStyle = this.adjustColor(color[1], -20); + ctx.moveTo(X, Y + H - S / 2); + ctx.lineTo(X + W, Y + H - S / 2); + ctx.moveTo(X + W - S / 2, Y); + ctx.lineTo(X + W - S / 2, Y + H); + ctx.stroke(); + } + fillGhostBlock(x: number, y: number, color: string[]) { + if (!this.ctx) return; + const ctx = this.ctx; + + const X = x * BLOCK; + const Y = y * BLOCK; + const W = BLOCK; + const H = BLOCK; + const S = 4; + + ctx.lineWidth = S; + ctx.beginPath(); + ctx.strokeStyle = this.adjustColor(color[0], -40); + ctx.moveTo(X, Y + S / 2); + ctx.lineTo(X + W, Y + S / 2); + ctx.moveTo(X + S / 2, Y); + ctx.lineTo(X + S / 2, Y + H); + ctx.stroke(); + + ctx.beginPath(); + ctx.strokeStyle = this.adjustColor(color[1], -60); + ctx.moveTo(X, Y + H - S / 2); + ctx.lineTo(X + W, Y + H - S / 2); + ctx.moveTo(X + W - S / 2, Y); + ctx.lineTo(X + W - S / 2, Y + H); + ctx.stroke(); + //ctx.strokeRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); + } + + clearBlock(x: number, y: number) { + if (!this.ctx) return; + const ctx = this.ctx; + ctx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)"; + ctx.fillRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2); + } + + drawHUD() { + if (!this.ctx || !this.canvas) return; + const ctx = this.ctx; + + if (this.garbage) + { + ctx.fillStyle ="red"; + ctx.fillRect(0, this.canvas.height - BLOCK * this.garbage, 6, BLOCK * this.garbage); + } + + ctx.fillStyle = "rgba(0,0,0,0.6)"; + ctx.fillRect(4, 4, 120, 60); + ctx.fillStyle = "#fff"; + ctx.font = "12px Kubasta"; + ctx.fillText(`${this.id == 0 ? p1_displayName : p2_displayName}: ${this.id == 0 ? p1_score : p2_score}`, 8, 20); + ctx.fillText(`score: ${this.score}`, 8, 36); + ctx.fillText(`lines: ${this.lines}`, 8, 52); + + if (this.isPaused) { + ctx.fillStyle = "rgba(0,0,0,0.7)"; + ctx.fillRect(0, this.canvas.height / 2 - 24, this.canvas.width, 48); + ctx.fillStyle = "#fff"; + ctx.font = "24px Kubasta"; + ctx.textAlign = "center"; + ctx.fillText( + "paused", + this.canvas.width / 2, + this.canvas.height / 2 + 8, + ); + ctx.textAlign = "start"; + } + + if (this.isGameOver) { + ctx.fillStyle = "rgba(0,0,0,0.7)"; + ctx.fillRect(0, this.canvas.height / 2 - 36, this.canvas.width, 72); + ctx.fillStyle = "#fff"; + ctx.font = "28px Kubasta"; + ctx.textAlign = "center"; + ctx.fillText( + "game over", + this.canvas.width / 2, + this.canvas.height / 2 + 8, + ); + ctx.textAlign = "start"; + } + } + + draw() { + if (!this.ctx || !this.canvas) return; + // clear everything + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + this.ctx.fillStyle = "#000"; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + this.ctx.strokeStyle = "#111"; + for (let r = 0; r <= ROWS; r++) { + this.ctx.beginPath(); + this.ctx.moveTo(0, r * BLOCK); + this.ctx.lineTo(COLS * BLOCK, r * BLOCK); + this.ctx.stroke(); + } + for (let c = 0; c <= COLS; c++) { + this.ctx.beginPath(); + this.ctx.moveTo(c * BLOCK, 0); + this.ctx.lineTo(c * BLOCK, ROWS * BLOCK); + this.ctx.stroke(); + } + + this.drawBoard(); + this.drawPiece(); + this.drawHUD(); + this.drawQueue(); + setSleepPos(); + } + } + + document.getElementById("game-retry")?.addEventListener("click", () => { document.getElementById("game-buttons")?.classList.add("hidden"); game1 = new Game("board1", 0); game2 = new Game("board2", 1); }); + + let p1_input: HTMLInputElement = document.getElementById("player1") as HTMLInputElement; + let p2_input: HTMLInputElement = document.getElementById("player2") as HTMLInputElement; + + p2_input.value = "player 2"; + if (await isLogged()) + { + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + p1_input.value = uuid; + p1_input.readOnly = true; + } + else + p1_input.value = "player 1"; + + document.getElementById("game-start")?.addEventListener("click", async () => { + let p1_isvalid = true; + let p2_isvalid = true; + if (await isLogged()) { + const p1_req = await fetch(`${user_api}/users/${p1_input.value}`, { + method: "GET", + credentials: "include", + }); + const p2_req = await fetch(`${user_api}/users/${p2_input.value}`, { + method: "GET", + credentials: "include", + }); + if (p1_req.status != 200) + p1_isvalid = false; + else + p1_displayName = (await p1_req.json()).displayName; + + if (p2_req.status != 200) + p2_isvalid = false; + else + p2_displayName = (await p2_req.json()).displayName; + } + else + p1_isvalid = p2_isvalid = false; + + p1_name = p1_input.value; + p2_name = p2_input.value; + if (!p1_isvalid) + p1_displayName = p1_name; + if (!p2_isvalid) + p2_displayName = p2_name; + + p1_displayName = p1_displayName.length > 16 ? p1_displayName.substring(0, 16) + "." : p1_displayName; + p2_displayName = p2_displayName.length > 16 ? p2_displayName.substring(0, 16) + "." : p2_displayName; + + document.getElementById("player-inputs").remove(); + document.getElementById("game-boards").classList.remove("hidden"); + game1 = new Game("board1", 0); + game2 = new Game("board2", 1); + }); + + } +} diff --git a/src/front/static/ts/views/TotpEnable.ts b/src/front/static/ts/views/TotpEnable.ts new file mode 100644 index 0000000..51efed2 --- /dev/null +++ b/src/front/static/ts/views/TotpEnable.ts @@ -0,0 +1,110 @@ +import { navigationManager, user_api, auth_api } from "../main.ts"; +import { dragElement } from "./drag.ts"; + +async function totpVerify() { + const code = (document.getElementById("totpPin") as HTMLInputElement).value; + const data_req = await fetch(auth_api + '/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:
${secret}
`; + 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
+ 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) +} diff --git a/src/front/static/ts/views/TournamentMenu.ts b/src/front/static/ts/views/TournamentMenu.ts new file mode 100644 index 0000000..e72b318 --- /dev/null +++ b/src/front/static/ts/views/TournamentMenu.ts @@ -0,0 +1,553 @@ +import Aview from "./Aview.ts" +import { isLogged, user_api } from "../main.js" +import { dragElement } from "./drag.ts"; +import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts" + +export default class extends Aview { + running: boolean; + + constructor() + { + super(); + this.setTitle("Tournament"); + setOnekoState("default"); + this.running = true; + } + + async getHTML() { + return ` +
+
+ pong_game.ts +
+ + + × +
+
+ +
+
+

how many players ?

+
+ + +
+
+
+ + + + +
+
+ `; + } + + async runGame(p1_id: number, p2_id: number, players: string[]): Promise { + return new Promise(async (resolve) => { + //console.log(p1_id, p2_id, players, players[p1_id], players[p2_id]); + let p1_name = players[p1_id]; + let p2_name = players[p2_id]; + + let uuid: string; + let start: number = 0; + let elapsed: number; + + let game_playing: boolean = false; + let match_over: boolean = false; + let p1_score: number = 0; + let p2_score: number = 0; + + let p1_displayName: string; + let p2_displayName: string; + + let countdown: number = 3; + let countdownTimer: number = 0; + + let canvas: HTMLCanvasElement; + let ctx: CanvasRenderingContext2D; + + const paddleOffset: number = 15; + const paddleHeight: number = 100; + const paddleWidth: number = 10; + const ballSize: number = 10; + + const paddleSpeed: number = 727 * 0.69; + let leftPaddleY: number; + let rightPaddleY: number; + let ballX: number; + let ballY: number; + let ballSpeed: number = 200; + let ballSpeedX: number = 300; + let ballSpeedY: number = 10; + + const keys: Record = {}; + + document.addEventListener("keydown", e => { keys[e.key] = true; }); + document.addEventListener("keyup", e => { keys[e.key] = false; }); + + function movePaddles() { + if ((keys["w"] || keys["W"]) && leftPaddleY > 0) + leftPaddleY -= paddleSpeed * elapsed; + if ((keys["s"] || keys["S"]) && leftPaddleY < canvas.height - paddleHeight) + leftPaddleY += paddleSpeed * elapsed; + if (keys["ArrowUp"] && rightPaddleY > 0) + rightPaddleY -= paddleSpeed * elapsed; + if (keys["ArrowDown"] && rightPaddleY < canvas.height - paddleHeight) + rightPaddleY += paddleSpeed * elapsed; + } + + function getBounceVelocity(paddleY: number) { + const paddleCenterY = paddleY + paddleHeight / 2; + + let n = (ballY - paddleCenterY) / (paddleHeight / 2); + n = Math.max(-1, Math.min(1, n)); + let theta = n * ((75 * Math.PI) / 180); + ballSpeedY = ballSpeed * Math.sin(theta); + } + + async function moveBall() { + let length = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY); + let scale = ballSpeed / length; + ballX += (ballSpeedX * scale) * elapsed; + ballY += (ballSpeedY * scale) * elapsed; + + if (ballY <= 0 || ballY >= canvas.height - ballSize) + ballSpeedY *= -1; + + if (ballX <= paddleWidth + paddleOffset && ballX >= paddleOffset && + ballY > leftPaddleY && ballY < leftPaddleY + paddleHeight) { + ballSpeedX *= -1; + ballX = paddleWidth + paddleOffset; + getBounceVelocity(leftPaddleY); + ballSpeed += 10; + } + + if (ballX >= canvas.width - paddleWidth - ballSize - paddleOffset && ballX <= canvas.width - ballSize - paddleOffset && + ballY > rightPaddleY && ballY < rightPaddleY + paddleHeight) { + ballSpeedX *= -1; + ballX = canvas.width - paddleWidth - ballSize - paddleOffset; + getBounceVelocity(rightPaddleY); + ballSpeed += 10; + } + + // scoring + if (ballX < 0 || ballX > canvas.width - ballSize) { + setOnekoState("default"); + game_playing = false; + if (ballX < 0) + p2_score++; + else + p1_score++; + + if (p1_score === 3 || p2_score === 3) { + if (await isLogged()) { + let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + fetch(`${user_api}/users/${uuid}/matchHistory?game=pong`, { + method: "POST", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ + "game": "pong", + "opponent": p2_name, + "myScore": p1_score, + "opponentScore": p2_score, + "date": Date.now(), + }), + }); + } + match_over = true; + resolve(p1_score == 3 ? p1_id : p2_id); + } + else { + countdown = 3; + countdownTimer = performance.now(); + } + + ballX = canvas.width / 2; + ballY = canvas.height / 2; + ballSpeed = 200; + ballSpeedX = 300 * ((ballSpeedX > 0) ? 1 : -1); + ballSpeedY = 10; + ballSpeedX = -ballSpeedX; + leftPaddleY = canvas.height / 2 - paddleHeight / 2; + rightPaddleY = canvas.height / 2 - paddleHeight / 2; + } + setBallPos(ballX, ballY); + } + + function draw() { + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.strokeStyle = "white"; + ctx.beginPath(); + ctx.setLineDash([5, 10]); + ctx.moveTo(canvas.width / 2, 0); + ctx.lineTo(canvas.width / 2, canvas.height); + ctx.stroke(); + + ctx.fillStyle = "white"; + ctx.fillRect(paddleOffset, leftPaddleY, paddleWidth, paddleHeight); + ctx.fillRect(canvas.width - paddleWidth - paddleOffset, rightPaddleY, paddleWidth, paddleHeight); + + ctx.fillStyle = "white"; + if (game_playing) + ctx.fillRect(ballX, ballY, ballSize, ballSize); + + ctx.font = "24px Kubasta"; + let text_score = `${p1_score} - ${p2_score}`; + ctx.fillText(text_score, canvas.width / 2 - (ctx.measureText(text_score).width / 2), 25); + ctx.fillText(p1_displayName, canvas.width / 4 - (ctx.measureText(p1_name).width / 2), 45); + ctx.fillText(p2_displayName, (canvas.width / 4 * 3) - (ctx.measureText(p2_name).width / 2), 45); + + if (match_over) { + ctx.font = "32px Kubasta"; + const winner = `${p1_score > p2_score ? p1_name : p2_name} won :D`; + ctx.fillText(winner, canvas.width / 2 - (ctx.measureText(winner).width / 2), canvas.height / 2 + 16); + document.getElementById("game-buttons")?.classList.remove("hidden"); + } + } + + function startCountdown() { + const now = performance.now(); + if (countdown > 0) { + if (now - countdownTimer >= 500) { + countdown--; + countdownTimer = now; + } + ctx.font = "48px Kubasta"; + ctx.fillText(countdown.toString(), canvas.width / 2 - 10, canvas.height / 2 + 24); + } + else if (countdown === 0) { + ctx.font = "48px Kubasta"; + ctx.fillText("Go!", canvas.width / 2 - 30, canvas.height / 2 + 24); + setTimeout(() => { + game_playing = true; + countdown = -1; + }, 500); + } + } + + const gameLoop = async (timestamp: number) => { + elapsed = (timestamp - start) / 1000; + start = timestamp; + if (game_playing) { + movePaddles(); + await moveBall(); + } + draw(); + if (!game_playing) + startCountdown(); + if (this.running) + requestAnimationFrame(gameLoop); + }; + + + document.getElementById("game-retry")?.addEventListener("click", () => { + setOnekoState("pong"); + document.getElementById("game-buttons")?.classList.add("hidden"); + game_playing = false; + match_over = false; + p1_score = 0; + p2_score = 0; + + countdown = 3; + countdownTimer = performance.now(); + }); + + let p1_isvalid = true; + let p2_isvalid = true; + if (await isLogged()) { + const p1_req = await fetch(`${user_api}/users/${p1_name}`, { + method: "GET", + credentials: "include", + }); + const p2_req = await fetch(`${user_api}/users/${p2_name}`, { + method: "GET", + credentials: "include", + }); + + if (p1_req.status == 200) + p1_displayName = (await p1_req.json()).displayName; + else + p1_displayName = p1_name; + + if (p2_req.status == 200) + p2_displayName = (await p2_req.json()).displayName; + else + p2_displayName = p2_name; + } + else + { + p1_displayName = p1_name; + p2_displayName = p2_name; + } + + p1_displayName = p1_displayName.length > 16 ? p1_displayName.substring(0, 16) + "." : p1_displayName; + p2_displayName = p2_displayName.length > 16 ? p2_displayName.substring(0, 16) + "." : p2_displayName; + p1_name = p1_name.length > 16 ? p1_name.substring(0, 16) + "." : p1_name; + p2_name = p2_name.length > 16 ? p2_name.substring(0, 16) + "." : p2_name; + document.getElementById("tournament-ui")?.classList.add("hidden"); + + canvas = document.createElement("canvas"); + canvas.id = "gameCanvas"; + canvas.classList.add("reverse-border"); + + document.getElementById("main-div")?.prepend(canvas); + + ctx = canvas.getContext("2d", { alpha: false }) as CanvasRenderingContext2D; + ctx.canvas.width = 600; + ctx.canvas.height = 600; + + leftPaddleY = canvas.height / 2 - paddleHeight / 2; + rightPaddleY = canvas.height / 2 - paddleHeight / 2; + ballX = canvas.width / 2; + ballY = canvas.height / 2; + + setOnekoState("pong"); + setOnekoOffset(); + requestAnimationFrame(gameLoop); + }); + } + + waitForUserClick(buttonId: string): Promise { + return new Promise((resolve) => { + const button = document.getElementById(buttonId); + if (!button) return resolve(); // failsafe if no button + + const handler = () => { + button.removeEventListener("click", handler); + resolve(); + }; + button.addEventListener("click", handler); + }); + } + + tournament_state: number[][]; + i: number = 0; + space: number; + + updateBracketDisplay(tournament: number[][], players: string[]) { + for (let i of Array(tournament[0].length).keys()) + this.tournament_state[this.i][i] = tournament[0][i]; + for (let i of Array(tournament[1].length).keys()) + { + console.log(this.tournament_state, this.i, i); + this.tournament_state[this.i + 1][i] = tournament[1][i]; + } + this.i++; + const container = document.getElementById("bracket-announcement"); + if (!container) return; + container.innerHTML = ""; // clear old bracket + + const bracketWrapper = document.createElement("div"); + bracketWrapper.className = "flex space-x-8 overflow-x-auto"; + + // replicate generateBracket() spacing logic + let previousPadding = 4; + + for (let round = 0; round < this.tournament_state.length; round++) { + const roundColumn = document.createElement("div"); + + if (round === 0) { + roundColumn.className = `flex flex-col mt-${this.space} space-y-4`; + } else { + previousPadding = previousPadding * 2 + 10; + roundColumn.className = `flex flex-col justify-center space-y-${previousPadding}`; + } + + // each player slot or winner + for (let i = 0; i < this.tournament_state[round].length; i++) { + const playerIndex = this.tournament_state[round][i]; + const name = + playerIndex !== undefined && playerIndex !== null + ? players[playerIndex] + : ""; + + const cell = document.createElement("div"); + cell.className = + "w-32 h-10 flex items-center justify-center bg-white text-center text-sm input-border"; + cell.textContent = name || ""; + roundColumn.appendChild(cell); + } + + bracketWrapper.appendChild(roundColumn); + } + + container.appendChild(bracketWrapper); + } + + async run() { + dragElement(document.getElementById("window")); + const generateBracket = async (playerCount: number) => { + this.tournament_state = []; + let initPlayerCount = playerCount; + document.getElementById("bracket").innerHTML = ""; + + const rounds: number = Math.ceil(Math.log2(playerCount)); + const totalSlots: number = 2 ** rounds; + let odd: number = 0; + let notPowPlayersCount: number = 0; + let tournament: number[][] = []; + + if ((playerCount & (playerCount - 1)) != 0) + notPowPlayersCount = playerCount - (2 ** Math.floor(Math.log2(playerCount))); + + + let initialPlayers = Array.from({ length: 2 ** Math.floor(Math.log2(playerCount))}, (_, i) => `player ${i + 1}`); + playerCount = 2 ** Math.floor(Math.log2(playerCount)); + + const bracketWrapper = document.createElement("div"); + bracketWrapper.className = "flex space-x-8 overflow-x-auto"; + + // Round 0: Player input column + const playerInputColumn = document.createElement("div"); + this.space = (notPowPlayersCount + odd) * 28; + playerInputColumn.className = `flex flex-col mt-${(notPowPlayersCount + odd) * 28} space-y-4`; + + tournament.push([]); + this.tournament_state.push([]); + initialPlayers.forEach((name, i) => { + const input = document.createElement("input"); + input.type = "text"; + input.id = `playerName${i}`; + input.value = name; + input.placeholder = name; + if (i == 0) + { + isLogged().then((value) => { + if (value) { + let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + input.value = uuid; + input.readOnly = true; + } + }); + } + input.className = "w-32 h-10 p-2 text-sm bg-white disabled:bg-gray-200 input-border"; + playerInputColumn.appendChild(input); + tournament[0].push(i); + this.tournament_state[0].push(-1); + }); + + bracketWrapper.appendChild(playerInputColumn); + + let currentRound = initialPlayers; + let previousPadding = 4; + tournament.push([]); + for (let round = 1; round <= rounds; round++) + { + this.tournament_state.push([]); + const roundColumn = document.createElement("div"); + previousPadding = previousPadding * 2 + 10 + roundColumn.className = `flex flex-col justify-center space-y-${previousPadding}`; + + const nextRound: string[] = []; + + while (notPowPlayersCount) { + tournament[1].push(playerCount); + this.tournament_state[1].push(-1); + const input = document.createElement("input"); + input.type = "text"; + input.id = `playerName${playerCount}`; + input.value = `player ${playerCount + 1}`; + input.placeholder = `player ${++playerCount}`; + input.className = + "w-32 h-10 p-2 text-sm bg-white disabled:bg-gray-200 input-border"; + roundColumn.appendChild(input); + --notPowPlayersCount; + nextRound.push(""); + } + + for (let i = 0; i < currentRound.length; i += 2) + { + const p1 = currentRound[i]; + const p2 = currentRound[i + 1]; + + const matchDiv = document.createElement("div"); + matchDiv.className = + "w-32 h-10 flex items-center justify-center bg-white text-center text-sm input-border"; + + matchDiv.textContent = ""; + nextRound.push(""); + + roundColumn.appendChild(matchDiv); + this.tournament_state[round].push(-1); + } + + bracketWrapper.appendChild(roundColumn); + currentRound = nextRound; + } + + document.getElementById("bracket")?.appendChild(document.createElement("hr")).classList.add("my-4", "mb-8", "w-64", "reverse-border"); + document.getElementById("bracket")?.appendChild(bracketWrapper); + const btn = document.getElementById("bracket")?.appendChild(document.createElement("button")); + if (!btn) return; + btn.classList.add("default-button", "w-full"); + btn.id = "tournament-play"; + btn.onclick = async () => { + document.getElementById("tournament-id")?.classList.add("hidden"); + let players: string[] = []; + let players_displayName: string[] = []; + for (let i of Array(initPlayerCount).keys()) { + players.push((document.getElementById(`playerName${i}`) as HTMLInputElement).value); + const name_req = await fetch(`${user_api}/users/${players.at(-1)}`, { + method: "GET", + credentials: "include", + }); + if (name_req.status === 200) + players_displayName.push((await name_req.json()).displayName); + else + players_displayName.push(players.at(-1)); + } + + while (tournament[0].length > 1) + { + this.updateBracketDisplay(tournament, players_displayName); + while(tournament[0].length > 0) + { + const p1 = tournament[0].shift() as number; + const p2 = tournament[0].shift() as number; + + document.getElementById("announcement-text").innerText = `${players_displayName[p1]} vs ${players_displayName[p2]}`; + document.getElementById("announcement")?.classList.remove("hidden"); + await this.waitForUserClick("tournament-continue"); + document.getElementById("announcement")?.classList.add("hidden"); + const result = await this.runGame(p1, p2, players); + document.getElementById("gameCanvas")?.remove(); + tournament[1].push(result); + } + tournament[0] = tournament[1]; + tournament[1] = []; + } + document.getElementById("winner-div")?.classList.remove("hidden"); + document.getElementById("winner-text").innerText = `${players_displayName[tournament[0][0]]} won the tournament !! ggs :D`; + }; + btn.innerText = "start tournament !!"; + + }; + + document.getElementById("bracket-generate")?.addEventListener("click", () => { + const input: HTMLInputElement = document.getElementById("playerNumber") as HTMLInputElement; + if (input.value == "") + return; + generateBracket(+input.value); + }); + } +} diff --git a/src/front/static/ts/views/drag.ts b/src/front/static/ts/views/drag.ts new file mode 100644 index 0000000..d1d7b32 --- /dev/null +++ b/src/front/static/ts/views/drag.ts @@ -0,0 +1,43 @@ +import { setOnekoOffset } from "../oneko.ts"; + +export function dragElement(el) { + var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + if (document.getElementById(el.id + "-header")) { + // if present, the header is where you move the DIV from: + document.getElementById(el.id + "-header").onmousedown = dragMouseDown; + } else { + // otherwise, move the DIV from anywhere inside the DIV: + el.onmousedown = dragMouseDown; + } + + function dragMouseDown(e) { + e = e || window.event; + e.preventDefault(); + // get the mouse cursor position at startup: + pos3 = e.clientX; + pos4 = e.clientY; + document.onmouseup = closeDragElement; + // call a function whenever the cursor moves: + document.onmousemove = elementDrag; + } + + function elementDrag(e) { + e = e || window.event; + e.preventDefault(); + // calculate the new cursor position: + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + // set the element's new position: + el.style.top = (el.offsetTop - pos2) + "px"; + el.style.left = (el.offsetLeft - pos1) + "px"; + setOnekoOffset(); + } + + function closeDragElement() { + // stop moving when mouse button is released: + document.onmouseup = null; + document.onmousemove = null; + } +} diff --git a/src/front/style.css b/src/front/style.css deleted file mode 100644 index b5c61c9..0000000 --- a/src/front/style.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/src/start.js b/src/start.js index 6175791..ad4cc0b 100644 --- a/src/start.js +++ b/src/start.js @@ -61,7 +61,7 @@ async function start() { if (target === 'scoreStore' || target === 'all') { const score = Fastify({ logger: loggerOption('scoreStore') }); score.register(scoreApi); - const port = target === 'all' ? 3002 : 3000; + const port = target === 'all' ? 3003 : 3000; const host = target === 'all' ? '127.0.0.1' : '0.0.0.0'; await score.listen({ port, host }); console.log(`ScoreStore API listening on http://${host}:${port}`); @@ -77,6 +77,6 @@ async function start() { } start().catch((err) => { - console.error(err); + console.log(err); process.exit(1); }); diff --git a/src/utils/authDB.js b/src/utils/authDB.js index a4f4595..a4611f3 100644 --- a/src/utils/authDB.js +++ b/src/utils/authDB.js @@ -82,9 +82,15 @@ function getUser(user) { return stmt.get(user); } +function rmUser(user) { + const stmt = database.prepare('DELETE FROM credentials WHERE username = ?'); + stmt.run(user); +} + const authDB = { prepareDB, checkUser, + rmUser, addUser, passwordQuery, setTOTPSecret, diff --git a/src/utils/authUserCreate.js b/src/utils/authUserCreate.js new file mode 100644 index 0000000..e8a6771 --- /dev/null +++ b/src/utils/authUserCreate.js @@ -0,0 +1,24 @@ +import axios from 'axios'; + +/** + * @param {string} username + * @param {import('fastify').FastifyInstance} fastify + */ +export async function authUserCreate(username, fastify) { + const payload = { + displayName: username, + }; + const cookie = fastify.jwt.sign({ user: "admin" }); + + const url = process.env.USER_URL || "http://localhost:3002" + + await axios.post( + url + "/users/" + username, + payload, + { + headers: { + 'Cookie': 'token=' + cookie, + }, + } + ); +} diff --git a/src/utils/authUserRemove.js b/src/utils/authUserRemove.js new file mode 100644 index 0000000..2fc3bfc --- /dev/null +++ b/src/utils/authUserRemove.js @@ -0,0 +1,19 @@ +import axios from 'axios' + +/** + * @param {string} username + * @param {import('fastify').FastifyInstance} fastify + */ +export async function authUserRemove(username, fastify) { + const url = (process.env.USER_URL || "http://localhost:3002") + "/users/" + username; + const cookie = fastify.jwt.sign({ user: "admin" }); + + await axios.delete( + url, + { + headers: { + 'Cookie': 'token=' + cookie, + }, + } + ); +} diff --git a/src/utils/scoreStore_contract.js b/src/utils/scoreStore_contract.js index b8ada16..d9f626b 100644 --- a/src/utils/scoreStore_contract.js +++ b/src/utils/scoreStore_contract.js @@ -29,7 +29,7 @@ async function loadContract() { async function callGetScore(id) { try { const contract = await loadContract(); - const result = await contract.getScore(id); + const result = await contract.getScore(id - 1); return result; } catch (error) { console.error('Error calling view function:', error); @@ -54,8 +54,9 @@ async function callAddScore(p1, p2, p1Score, p2Score) { const tx = await contract.addScore(p1, p2, p1Score, p2Score); console.log('Transaction sent:', tx.hash); await tx.wait(); // Wait for the transaction to be mined + const id = await callLastId(); console.log('Transaction confirmed'); - return tx; + return { tx, id }; } catch (error) { console.error('Error calling addScore function:', error); throw error; diff --git a/tailwind.config.js b/tailwind.config.js index d2fdab4..53376f7 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,7 +1,13 @@ export default { - content: ['./src/front/**/*.{html,js}'], + content: ['./src/front/**/*.{html,js,ts,css}'], theme: { - extend: {}, + extend: { + fontFamily: { + jersey: ['"Jersey 10"', 'sans-serif'], + }, + }, + }, + }, }, plugins: [], }