Merge pull request #64 from KeyZox71/main

push to prod
This commit is contained in:
Adam
2025-10-24 16:56:39 +02:00
committed by GitHub
100 changed files with 7106 additions and 2804 deletions

View File

@ -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=<private-key>
AVAX_RPC_URL=<url>
AVAX_CONTRACT_ADDR=<pub key of contract>
SMTP_SMARTHOST=<the host of the smtp server>
SMTP_FROM=<the address to send from>
SMTP_AUTH_USERNAME=<smtp-user>
SMTP_AUTH_PASSWORD=<smtp pass>
EMAIL_TO=<mail to send to>
# all of those can't have a / at the env ↓
USER_URL=<the internal url to the user api>
AUTH_URL=<the internal url to the auth api>
SCORE_URL=<the internal url to the score store api>
CORS_ORIGIN=<the external url of origin for cors>
VITE_USER_URL=<the external url of the user api>
VITE_AUTH_URL=<the external url of the auth api>

View File

@ -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
docker compose -f docker/docker-compose.yml ps

View File

@ -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

View File

@ -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

68
doc/auth/2fa.md Normal file
View File

@ -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": "<the generated secret>"
"otpauthUrl": "<the generated url>"
}
```
## POST `/2fa/verify`
Used to confirm 2fa
Inputs: a valid JWT in cookie and
```json
{
"token": "<token given by 2fa>"
}
```
Returns:
- 200
```json
{
"msg": "2FA verified successfully"
}
```
- 401 || 400 || 404
```json
{
"error": "<corresponding error>"
}
```
## DELETE `/2fa`
Used to remove 2fa
Inputs: a valid JWT in cookie
Returns:
- 200
```json
{
"msg": "TOTP removed"
}
```

72
doc/auth/login.md Normal file
View File

@ -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": "<string>",
"password": "<string>",
(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": "<corresponding 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": "<corresponding error>"
}
```
- 200 with response and cookie in header
```json
{
"msg": "Login successfully"
}
```

24
doc/auth/logout.md Normal file
View File

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

11
doc/auth/me.md Normal file
View File

@ -0,0 +1,11 @@
GET `/me`
Inputs : just need the JWT cookie
Returns the user of the account
```
{
user: ":userId"
}
```

65
doc/auth/register.md Normal file
View File

@ -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": "<string>",
"password": "<string>"
}
```
Can return:
- 200 with response and cookie in header
```json
{
"msg": "Register successfully"
}
```
- 400 with response
```json
{
"error": "<corresponding 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": "<corresponding error>"
}
```
- 200 with response and cookie in header
```json
{
"msg": "Register successfully"
}
```

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

@ -0,0 +1,32 @@
# remove user
Available endpoints:
- DELETE `/`
Common return:
- 500 with response
```json
{
"error": "Internal server error"
}
```
## DELETE `/`
User to remove a user from the backend
Inputs: just need a valid JWT cookie
Returns:
- 200
```json
{
"msg": "User successfully deleted"
}
```
- 401 || 400
```json
{
"error": "<corresponding msg>
}
```

56
doc/scoreStore/README.md Normal file
View File

@ -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": "<the name of the p1>",
"p2": "<the name of the p2>",
"p1Score": "<the score of the p1>",
"p2Score": "<the score of the p2>"
},
"tx": "<the transcaction hash>"
}
```
## 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": "<name of the p1>",
"p2": "<name of the p2>",
"p1Score": "<score of the p1>",
"p2Score": "<score of the p2>"
}
```
Returns:
- 200
```json
{
"id": "<the id of the added score>"
}
```

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

@ -0,0 +1,120 @@
# Avatar
Available endpoints:
- POST `/users/:userId/avatar`
- GET `/users/:userId/avatar`
- PATCH `/users/:userId/avatar`
- DELETE `/users/:userId/avatar`
Common return:
- 500 with response
```json
{
"error": "Internal server error"
}
```
## POST /users/:userId/avatar
Used to upload an avatar
Input needed :
```json
{
<FormData object containing the file>
}
```
Can return:
- 200 with response
```json
{
"msg": "Avatar uploaded successfully"
}
```
- 400 with response (if the file is too large, or file is missing, or it is missing a file name, or it is missing a mime type)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if the user does not exist)
```json
{
"error": "<corresponding error>"
}
```
## GET /users/:userId/avatar
Used to download an avatar
Input needed :
```json
{
<FormData object containing the file>
}
```
Can return:
- 200 with response
```json
{
"msg": "Avatar uploaded successfully"
}
```
- 404 with response (if the user does not exist, or the user does not have an assigned avatar, or the image does not exist)
```json
{
"error": "<corresponding error>"
}
```
## PATCH /users/:userId/avatar
Used to modify an avatar
Input needed :
```json
{
<FormData object containing the file>
}
```
Can return:
- 200 with response
```json
{
"msg": "Avatar modified successfully"
}
```
- 400 with response (if the file is too large, or file is missing, or it is missing a file name, or it is missing a mime type)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if the user does not exist)
```json
{
"error": "<corresponding error>"
}
```
## DELETE /users/:userId/avatar
Used to delete an avatar
Can return:
- 200 with response
```json
{
"msg": "Avatar deleted successfully"
}
```
- 404 with response (if the user does not exist, or the user does not have an assigned avatar)
```json
{
"error": "<corresponding error>"
}
```

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

@ -0,0 +1,155 @@
# Friend
Available endpoints:
- POST `/users/:userId/friends`
- GET `/users/:userId/friends`
- GET `/users/:userId/friends/count`
- DELETE `/users/:userId/friends`
- DELETE `/users/:userId/friends/:friendId`
Common return:
- 500 with response
```json
{
"error": "Internal server error"
}
```
## POST `/users/:userId/friends/:friendId`
Used to add a friend to an user
Can return:
- 200 with response
```json
{
"msg": "Friend added successfully"
}
```
- 400 with response (if no user is specified in header, or friend is the user specified in header, or friend is already added)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist, or friend does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 401 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users/:userId/friends?iStart=<starting index (included)>&iEnd=<ending index (excluded)>`
Used to get the friends of an user
Can return:
- 200 with response (list of friend objects (between iStart and iEnd))
```json
{
"friends":
[
{
"friendName": "<the friend's username>",
"friendDisplayName": "<the friend's display name>"
},
...
]
}
```
- 400 with response (if iStart/iEnd is missing, or iEnd < iStart)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist, or no friends exist in the selected range)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users/:userId/friends/count`
Used to get the number of friends of an user
Can return:
- 200 with response
```json
{
"n_friends": <number of friends>
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```
## DELETE `/users/:userId/friends`
Used to delete the friends of an user
Can return:
- 200 with response
```json
{
"msg": "Friends deleted successfully"
}
```
- 400 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
- 401 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```
## DELETE `/users/:userId/friends/:friendId`
Used to delete a friend of an user
Can return:
- 200 with response
```json
{
"msg": "Friend deleted successfully"
}
```
- 400 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
- 401 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist, or friend does not exist)
```json
{
"error": "<corresponding error>"
}
```

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

@ -0,0 +1,148 @@
# Match History
Available endpoints:
- POST `/users/:userId/matchHistory`
- GET `/users/:userId/matchHistory`
- GET `/users/:userId/matchHistory/count`
- DELETE `/users/:userId/matchHistory`
Common return:
- 500 with response
```json
{
"error": "Internal server error"
}
```
## POST `/users/:userId/matchHistory?game=<pong/tetris>`
Used to add a match result to an user to a specific game
Input needed :
```json
{
"game": "<pong/tetris>"
"opponent": "<the opponent's username>", <= item only present if the match involved 2 players
"myScore": <my score>,
"opponentScore": <the opponent's score>, <= item only present if the match involved 2 players
"date": <seconds since Epoch (Date.now() return)>
}
```
Can return:
- 200 with response
```json
{
"msg": "Match successfully saved to the blockchain"
}
```
- 400 with response (if no user is specified in header, or no opponent/p1Score/p2Score is specified in body, or opponent is the user specified in header, or a score is negative, or the game specified is invalid, or the game should involve more players than was specified)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist, or opponent does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 401 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users/:userId/matchHistory?game=<pong/tetris>&iStart=<starting index (included)>&iEnd=<ending index (excluded)>`
Used to get the match history of an user for a specific game
Can return:
- 200 with response (list of matches results (between iStart and iEnd))
```json
{
"matchHistory":
[
{
"score":
{
"p1": "<the name of the p1>",
"p2": "<the name of the p2>", <= item only present if the match involved 2 players
"p1Score": "<the score of the p1>",
"p2Score": "<the score of the p2>", <= item only present if the match involved 2 players
"date": <seconds since Epoch (Date.now() return)>
},
"tx": "<the transcaction hash>"
},
...
]
}
```
- 400 with response (if iStart/iEnd does not exist, or iEnd < iStart, or the game specified is invalid)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist, or no matches exist in the selected range)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users/:userId/matchHistory/count?game=<pong/tetris>`
Used to get the number of matches an user played for a specific game
Can return:
- 200 with response
```json
{
"n_matches": <number of matches played by the user>
}
```
- 400 with response (if game does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```
## DELETE `/users/:userId/matchHistory?game=<pong/tetris>`
Used to delete the match history of an user for a specific game
Can return:
- 200 with response
```json
{
"msg": "Match history deleted successfully"
}
```
- 400 with response (if user specified in header is neither admin nor user, or the game specified is invalid)
```json
{
"error": "<corresponding error>"
}
```
- 401 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```

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

@ -0,0 +1,41 @@
# ping
Available endpoints:
- POST `/ping`
- GET `/ping/:userId`
Common return:
- 500 with response
```json
{
"error": "Internal server error"
}
```
## POST `/ping`
Used to send a ping and update the lastSeenTime (can be used for activity time)
Input needed : just need a valid token
Can return:
- 200
```json
{
"msg": "last seen time updated successfully"
}
```
## GET `/ping/:userId`
Used to retrive the lastSeenTime of a user
Input needed : just need a valid token
Can return:
- 200
```json
{
"isLogged": "<true/false>"
}
```

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

@ -0,0 +1,212 @@
# User
Available endpoints:
- POST `/users/:userId`
- GET `/users`
- GET `/users/count`
- GET `/users/:userId`
- PATCH `/users/:userId/:member`
- DELETE `/users/:userId`
- DELETE `/users/:userId/:member`
Common return:
- 500 with response
```json
{
"error": "Internal server error"
}
```
## POST `/users/:userId`
Used to create an user
Input needed :
```json
{
"displayName": "<the display name>"
}
```
Can return:
- 200 with response
```json
{
"msg": "User created successfully"
}
```
- 400 with response (if no user is specified in header, or user already exists, or no display name is specified in body)
```json
{
"error": "<corresponding error>"
}
```
- 401 with response (if user specified in header is not admin)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users?iStart=<starting index (included)>&iEnd=<ending index (excluded)>`
Used to get the list of users
Can return:
- 200 with response (list of user objects (between iStart and iEnd))
```json
{
"users":
[
{
"username": "<the username>",
"displayName": "<the display name>",
"pong": {
"wins": <the number of pong matches won>,
"losses": <the number of pong matches lost>
},
"tetris": {
"wins": <the number of tetris matches won>,
"losses": <the number of tetris matches lost>
}
},
...
]
}
```
- 400 with response (if iStart/iEnd is missing, or iEnd < iStart)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if no users exist in the selected range)
```json
{
"error": "<corresponding error>"
}
```
## GET `/users/count`
Used to get the number of users
Always returns:
- 200 with response
```json
{
"n_users": <number of users>
}
```
## GET `/users/:userId`
Used to get an user
Can return:
- 200 with response (an user object)
```json
{
"username": "<the username>",
"displayName": "<the display name>",
"pong": {
"wins": <the number of pong matches won>,
"losses": <the number of pong matches lost>
},
"tetris": {
"wins": <the number of tetris matches won>,
"losses": <the number of tetris matches lost>
}
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```
## PATCH `/users/:userId/:member`
Used to modify a member of an user (only displayName can be modified)
Input needed :
```json
{
"<the member to modify (must be identical to :member)>": "<the member's new value>"
}
```
Can return:
- 200 with response
```json
{
"msg": "<:member> modified sucessfully"
}
```
- 400 with response (if no user is specified in header, or new value of member to modify is not provided in the body, or member does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 401 with response (if user specified in header is not admin)
```json
{
"error": "<corresponding error>"
}
```
## DELETE `/users/:userId`
Used to delete an user
Can return:
- 200 with response
```json
{
"msg": "User deleted successfully"
}
```
- 404 with response (user does not exist)
```json
{
"error": "<corresponding error>"
}
```
## DELETE `/users/:userId/:member`
Used to delete a member of an user (only displayName can be deleted)
Can return:
- 200 with response
```json
{
"msg": "<:member> deleted successfully"
}
```
- 401 with response (if user specified in header is neither admin nor user)
```json
{
"error": "<corresponding error>"
}
```
- 400 with response (if no user is specified in header, or member to delete does not exist)
```json
{
"error": "<corresponding error>"
}
```
- 404 with response (if user does not exist)
```json
{
"error": "<corresponding error>"
}
```

View File

@ -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:

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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
}

File diff suppressed because it is too large Load Diff

View File

@ -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']

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

6
flake.lock generated
View File

@ -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": {

View File

@ -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
'';

View File

@ -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",

275
pnpm-lock.yaml generated
View File

@ -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:

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

18
src/api/auth/logout.js Normal file
View File

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

View File

@ -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

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

@ -0,0 +1,35 @@
import authDB from '../../utils/authDB.js';
import { authUserRemove } from '../../utils/authUserRemove.js';
/**
* @param {import('fastify').FastifyRequest} request
* @param {import('fastify').FastifyReply} reply
* @param {import('fastify').FastifyInstance} fastify
*/
export async function remove(request, reply, fastify) {
try {
const user = request.user;
if (authDB.RESERVED_USERNAMES.includes(user)) {
return reply.code(400).send({ error: 'Reserved username' });
}
if (authDB.checkUser(user) === false) {
return reply.code(400).send({ error: "User does not exist" });
}
authDB.rmUser(user)
authUserRemove(user, fastify)
return reply
.code(200)
.clearCookie("token")
.send({
msg: "User successfully deleted"
})
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,24 @@
import authDB from '../../utils/authDB.js';
/**
* @param {import('fastify').FastifyRequest} request
* @param {import('fastify').FastifyReply} reply
*/
export async function totpCheck(request, reply) {
try {
const user = request.user;
if (authDB.checkUser(user) === false) {
return reply.code(400).send({ error: "User does not exist" });
}
return reply
.code(200)
.send({
totp: authDB.isTOTPEnabled(user)
});
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

@ -9,12 +9,11 @@ import { callAddScore, callLastId } from "../../utils/scoreStore_contract.js";
*/
export async function addTx(request, reply, fastify) {
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)

View File

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

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

@ -0,0 +1,24 @@
export async function dAvatar(request, reply, fastify, getUserInfo, getAvatarId, deleteAvatarId, deleteImage) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
if (!getUserInfo.get(userId)) {
return reply.cose(404).send({ error: "User does not exist" });
}
const imageId = getAvatarId.get(userId);
if (imageId.avatarId === -1) {
return reply.code(404).send({ error: "User does not have an avatar" });
}
deleteImage.run(imageId.avatarId);
deleteAvatarId.run(userId);
return reply.code(200).send({ msg: "Avatar deleted successfully" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,23 @@
export async function dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
if (request.user !== 'admin' && request.user !== userId) {
return reply.code(401).send({ error: "Unauthorized" });
}
const friendId = request.params.friendId;
if (!getFriend.get(userId, friendId)) {
return reply.code(404).send({ error: "Friend does not exist" });
}
deleteFriend.run(userId, friendId);
return reply.code(200).send({ msg: "Friend deleted successfully" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,16 @@
export async function dFriends(request, reply, fastify, getUserInfo, deleteFriends) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
deleteFriends.run(userId);
return reply.code(200).send({ msg: "Friends deleted successfully" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

@ -0,0 +1,26 @@
export async function dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
const { game } = request.query;
if (game !== 'pong' && game !== 'tetris') {
return reply.code(400).send({ error: "Specified game does not exist" });
}
deleteMatchHistory.run(game, userId);
if (game === 'pong') {
deleteStatsPong.run(userId);
}
else if (game === 'tetris') {
deleteStatsTetris.run(userId);
}
return reply.code(200).send({ msg: "Match history deleted successfully" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,22 @@
export async function dMember(request, reply, fastify, getUserInfo, changeDisplayName) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
const user = request.user;
const member = request.params.member;
if (member === 'displayName') {
changeDisplayName.run("", request.params.userId);
return reply.code(200).send({ msg: "Display name deleted successfully" });
} else {
return reply.code(400).send({ msg: "Member does not exist" })
}
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,19 @@
export async function dUser(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteFriends, deleteUser) {
try {
if (request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
const userId = request.params.userId;
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
deleteMatchHistory.run('pong', userId);
deleteMatchHistory.run('tetris', userId);
deleteFriends.run(userId);
deleteUser.run(userId);
return reply.code(200).send({ msg: "User deleted successfully" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

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

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

@ -0,0 +1,23 @@
export async function gAvatar(request, reply, fastify, getUserInfo, getAvatarId, getImage) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
const imageId = getAvatarId.get(userId);
if (imageId.avatarId === -1) {
return reply.code(404).send({ error: "User does not have an avatar" });
}
const image = getImage.get(imageId.avatarId);
if (!image) {
return reply.code(404).send({ error: "Avatar does not exist" });
}
return reply.code(200).type(image.mimeType).send(image.data);
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,29 @@
export async function gFriends(request, reply, fastify, getUserInfo, getFriends) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
const { iStart, iEnd } = request.query;
if (Number(iEnd) < Number(iStart)) {
return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" });
}
const friendNames = getFriends.all(userId, Number(iEnd) - Number(iStart), Number(iStart));
if (!friendNames.length) {
return reply.code(404).send({ error: "No friends exist in the selected range" });
}
const promises = friendNames.map(async (friendName) => {
const friend = getUserInfo.get(friendName.friendName);
friendName.friendDisplayName = friend.displayName;
return friendName;
});
const friends = await Promise.all(promises);
return reply.code(200).send({ friends });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

@ -0,0 +1,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" });
}
}

View File

@ -0,0 +1,17 @@
export async function gNumberFriends(request, reply, fastify, getUserInfo, getNumberFriends) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
const row = getNumberFriends.get(userId);
return reply.code(200).send({ n_friends: row.n_friends });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

@ -0,0 +1,20 @@
export async function gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
const { game } = request.query;
if (game !== 'pong' && game !== 'tetris') {
return reply.code(400).send({ error: "Specified game does not exist" });
}
const row = getNumberMatches.get(game, userId);
return reply.code(200).send({ n_matches: row.n_matches });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

@ -0,0 +1,9 @@
export async function gNumberUsers(request, reply, fastify, getNumberUsers) {
try {
const row = getNumberUsers.get();
return reply.code(200).send({ n_users: row.n_users });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,28 @@
/**
* @param {import('fastify').FastifyRequest} request
* @param {import('fastify').FastifyReply} reply
* @param {import('fastify').FastifyInstance} fastify
*/
export async function gPing(request, reply, fastify, getActivityTime) {
try {
const user = request.params.userId;
const time = getActivityTime.get(user);
if (!time || !time.time) {
return reply.code(404).send({ error: "User not found or no activity time recorded" });
}
const lastSeenTime = new Date(time.time);
const now = new Date();
const oneMinuteAgo = new Date(now.getTime() - 60000); // 60,000 ms = 1 minute
const isActiveInLastMinute = lastSeenTime >= oneMinuteAgo;
return reply.code(200).send({
isLogged: isActiveInLastMinute
});
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,24 @@
export async function gUser(request, reply, fastify, getUserInfo) {
try {
const userId = request.params.userId;
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
const userInfo = getUserInfo.get(userId);
return reply.code(200).send({
username: userInfo.username,
displayName: userInfo.displayName,
pong: {
wins: userInfo.pongWins,
losses: userInfo.pongLosses
},
tetris: {
wins: userInfo.tetrisWins,
losses: userInfo.tetrisLosses
}
});
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,28 @@
export async function gUsers(request, reply, fastify, getUserData) {
try {
const { iStart, iEnd } = request.query;
if (Number(iEnd) < Number(iStart)) {
return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" });
}
const users = getUserData.all(Number(iEnd) - Number(iStart), Number(iStart));
if (!users.length) {
return reply.code(404).send({ error: "No users exist in the selected range" });
}
const usersFormat = users.map(obj => ({
username: obj.username,
displayName: obj.displayName,
pong: {
wins: obj.pongWins,
losses: obj.pongLosses
},
tetris: {
wins: obj.tetrisWins,
losses: obj.tetrisLosses
}
}));
return reply.code(200).send({ usersFormat });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,45 @@
import sharp from 'sharp';
/**
* @param {import('fastify').FastifyRequest} request
* @param {import('fastify').FastifyReply} reply
* @param {import('fastify').FastifyInstance} fastify
*/
export async function pAvatar(request, reply, fastify, getUserInfo, setAvatarId, postImage) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
// Read the raw body as a Buffer
const buffer = request.body;
if (!buffer) {
return reply.code(400).send({ error: "No file uploaded" });
}
// Check file size (5MB limit)
if (buffer.length > 5 * 1024 * 1024) {
return reply.code(400).send({ error: "File too large" });
}
// Convert to WebP
const webpBuffer = await sharp(buffer).toFormat('webp').toBuffer();
// Save the image and update the user's avatar
const mimeType = request.headers['content-type'];
const fileName = `avatar_${userId}.webp`;
const imageId = postImage.run(fileName, mimeType, webpBuffer);
setAvatarId.run(imageId.lastInsertRowid, userId);
return reply.code(200).send({ msg: "Avatar uploaded successfully" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,26 @@
export async function pFriend(request, reply, fastify, getUserInfo, getFriend, addFriend) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
const friendId = request.params.friendId;
if (!getUserInfo.get(friendId)) {
return reply.code(404).send({ error: "Friend does not exist" });
}
if (friendId === userId) {
return reply.code(400).send({ error: "You can't add yourself :D" });
}
if (getFriend.get(userId, friendId)) {
return reply.code(400).send({ error: "Friend already added" });
}
addFriend.run(userId, friendId)
return reply.code(200).send({ msg: "Friend added successfully" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

@ -0,0 +1,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" });
}
}

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

@ -0,0 +1,21 @@
/**
* @param {import('fastify').FastifyRequest} request
* @param {import('fastify').FastifyReply} request
* @param {import('fastify').Fastify} fastify
*/
export async function pPing(request, reply, fastify, setActivityTime) {
try {
const user = request.user;
const currentTime = new Date().toISOString();
setActivityTime.run(user, currentTime);
return reply.code(200)
.send({
msg: "last seen time updated successfully"
});
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

@ -0,0 +1,19 @@
export async function pUser(request, reply, fastify, getUserInfo, createUser) {
try {
const userId = request.params.userId;
if (request.user !== 'admin') {
return reply.code(401).send({ error: "Unauthorized" });
}
if (getUserInfo.get(userId)) {
return reply.code(400).send({ error: "User already exist" });
}
if (!request.body || !request.body.displayName) {
return reply.code(400).send({ error: "Please specify a display name" });
}
createUser.run(userId, request.body.displayName);
return reply.code(200).send({ msg: "User created successfully" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

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

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

@ -0,0 +1,23 @@
export async function uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
const member = request.params.member;
if (member === 'displayName') {
if (!request.body || !request.body.displayName) {
return reply.code(400).send({ error: "Please specify a displayName" });
}
changeDisplayName.run(request.body.displayName, userId);
return reply.code(200).send({ msg: "Display name modified successfully" });
}
return reply.code(400).send({ error: "Member does not exist" })
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

@ -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"}]

View File

@ -1,19 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Tailwind Test</title>
<link href="/style.css" rel="stylesheet" />
</head>
<body class="bg-gray-100 flex items-center justify-center min-h-screen">
<div class="text-center p-10 bg-white rounded-xl shadow space-y-4">
<h1 class="text-4xl font-bold text-blue-600">Vite + Tailwind</h1>
<p class="text-gray-700 text-lg">🚀 Looks like it's working!</p>
<button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
Click Me
</button>
</div>
</body>
</html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/static/assets/favicon.ico" rel="icon" type="image/x-icon" >
<title>Vite + Tailwind Test</title>
<link href="/static/css/style.css" rel="stylesheet" type="text/css" />
</head>
<body class="bg-[url(https://api.kanel.ovh/random)] dark:bg-[url(https://api.kanel.ovh/random)] bg-center bg-cover h-screen flex flex-col font-[Lexend]">
<div class="absolute flex flex-col items-center space-y-5 top-4 left-5">
<!--a class="absolute flex flex-col items-center top-4 left-5" href="/pong" data-icon-->
<a class="flex flex-col items-center" href="/pong" data-icon>
<img src="./static/assets/pong.svg" width=32 height=32 />
<span class="text-white font-[Kubasta]">pong_game.ts</span>
</a>
<a class="flex flex-col items-center" href="/tetris" data-icon>
<img src="./static/assets/tetrio.svg" width=32 height=32 />
<span class="text-white font-[Kubasta]">tetris_game.ts</span>
</a>
<!--a class="flex flex-col items-center" href="https://tetr.io/">
<img src="./static/assets/tetrio.svg" width=32 height=32 />
<span class="text-white font-[Kubasta]">tetr.io</span>
</a-->
</div>
<div id="app" class="flex-1 flex items-center justify-center">
</div>
<div id="taskbar-menu" class="absolute bottom-13 left-0"></div>
<div id="friends-menu" class="absolute bottom-13 right-0"></div>
<div class="border-t-2 border-neutral-300 dark:border-neutral-800 sticky bottom-0">
<nav class="bg-neutral-200 dark:bg-neutral-900 shadow-md border-t-2 border-neutral-400 dark:border-neutral-700 flex justify-between h-12 items-center content-center space-x-6 font-[Kubasta]">
<div class="flex px-4 items-center content-center space-x-2">
<button id="profile-button" class="taskbar-button flex flex-row justify-center items-center"><img id="start-img" class="object-scale-down mr-2 h-5 w-5" src="https://api.kanel.ovh/id?id=65" /> start</button>
<div class="text-neutral-700 dark:text-neutral-400">|</div>
<a target="_blank" class="taskbar-button" href="https://rusty.42angouleme.fr/">rusty</a>
<a target="_blank" class="taskbar-button" href="https://dn720004.ca.archive.org/0/items/2009-tetris-variant-concepts_202201/2009%20Tetris%20Design%20Guideline.pdf">tetris-guideline.pdf</a>
</div>
<div id="taskbar-trail" class="flex px-4 items-center content-center space-x-2">
<div class="reverse-border m-1.5 h-8/10 content-center">
<span id="taskbar-clock" class="text-neutral-900 dark:text-white px-4">12:37</span>
</div>
</div>
</nav>
</div>
<script type="module" src="/static/ts/main.ts"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,17 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 1080" width="1080" height="1080">
<defs>
<image width="1080" height="1080" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4AQMAAADW3v7MAAAAAXNSR0IB2cksfwAAAANQTFRFAAAAp3o92gAAA0hJREFUeJztzjEBACAMwDDm3zTPniiAo1GQOX+Y14HVQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQz3UQxfkswQ5pU9WdQAAAABJRU5ErkJggg=="/>
<image width="97" height="97" id="img2" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGEAAABhAQMAAAD8yF3gAAAAAXNSR0IB2cksfwAAAAZQTFRF////////VXz1bAAAAAJ0Uk5T/v/Rru6GAAAAHUlEQVR4nGOsb2CAg0bGUd4ob5Q3yhvlEctjQAYA62mQYokW47wAAAAASUVORK5CYII="/>
<image width="97" height="97" id="img3" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGEAAABhAQMAAAD8yF3gAAAAAXNSR0IB2cksfwAAAAZQTFRF////////VXz1bAAAAAJ0Uk5T/wDltzBKAAAAHUlEQVR4nGNkQAaMo7xR3ihvpPL+M/xnGuVRwAMA3qIuYqMkUaQAAAAASUVORK5CYII="/>
<image width="97" height="97" id="img4" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGEAAABhAgMAAAC7aCcwAAAAAXNSR0IB2cksfwAAAAlQTFRF////////////jkrl/gAAAAN0Uk5T/v8A1ghndgAAACtJREFUeJxjFHVgwApeM47KjMqMyozKjMqMyozK0FJmFYj6v2ZUZlSGejIAqSynoiv80kgAAAAASUVORK5CYII="/>
</defs>
<style>
.s0 { fill: #ffffff }
</style>
<use id="Background" href="#img1" x="0" y="0"/>
<path id="Layer 1" fill-rule="evenodd" class="s0" d="m1008 746v97h-937v-97z"/>
<path id="Shape 1" fill-rule="evenodd" class="s0" d="m729 459v97h-97v-97z"/>
<use id="Shape 1 copy" style="opacity: .7" href="#img2" x="706" y="385"/>
<use id="Shape 1 copy 2" style="opacity: .4" href="#img3" x="780" y="311"/>
<use id="Shape 1 copy 3" style="opacity: .2" href="#img4" x="854" y="237"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="60mm"
height="60mm"
viewBox="0 0 60 60"
version="1.1"
id="svg9638"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="tetrio.svg"
inkscape:export-filename="D:\Projects\tetrio\client\res\tetriox256.png"
inkscape:export-xdpi="108.37334"
inkscape:export-ydpi="108.37334">
<defs
id="defs9632" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="121.84373"
inkscape:cy="101.26992"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:pagecheckerboard="true"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1676"
inkscape:window-y="-4"
inkscape:window-maximized="1" />
<metadata
id="metadata9635">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-237)">
<rect
style="opacity:0.07000002;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.29149818;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect14257"
width="55"
height="55"
x="2.05"
y="239.5" />
<rect
style="opacity:0.07000002;fill:#000000;fill-opacity:1;stroke:none;stroke-width:20.26499748;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect14257-7"
width="50"
height="50"
x="5"
y="242" />
<rect
style="opacity:0.07000002;fill:#0e0b0e;fill-opacity:1;stroke:none;stroke-width:18.23849869;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect14257-7-6"
width="45"
height="45"
x="7.5"
y="244.5" />
<path
style="opacity:1;fill:#df4eaa;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 0,237 v 10 h 10 v 10 h 10 v -10 h 10 v -10 z"
id="path14118"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<rect
style="opacity:1;fill:#c040aa;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect14120"
width="10"
height="40"
x="10"
y="257" />
<rect
style="opacity:1;fill:#7e5fe3;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect14120-0"
width="10"
height="40"
x="20"
y="257" />
<path
style="opacity:1;fill:#2f51aa;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 30,287 H 50 V 277 H 40 V 257 H 30 Z"
id="path14118-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<path
style="opacity:1;fill:#15919d;fill-opacity:1;stroke:none;stroke-width:20.26499939;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 60,247 H 40 v 10 h 10 v 20 h 10 z"
id="path14118-8-3"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

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

174
src/front/static/ts/main.ts Normal file
View File

@ -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<boolean> {
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();

View File

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

View File

@ -0,0 +1,10 @@
export default class {
contructor()
{
}
setTitle(title) { document.title = title; }
async getHTML() { return ""; }
async run() { }
};

View File

@ -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 `
<div class="relative b-0 default-border bg-neutral-200">
<div class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-40 justify-between px-2">
<span class="font-[Kubasta]">friends.ts</span>
</div>
<div class="bg-neutral-200 pb-5 dark:bg-neutral-800 justify-center text-center reverse-border">
<form method="dialog" class="justify-center bg-neutral-200 dark:bg-neutral-800 space-y-4 space-x-2 px-4 pt-4">
<input type="text" id="new-friend" placeholder="new friend" class="bg-white text-neutral-900 input-border" required></input>
<button id="add-friends-button" type="submit" class="default-button text-center mx-0 my-0">add friend</button>
<p id="add-friend-err" class="hidden text-red-700 dark:text-red-500"></p>
<p id="add-friend-msg" class="hidden text-gray-900 dark:text-white text-lg"></p>
</form>
<p id="friends-error-message" class="hidden text-red-700 dark:text-red-500"></p>
<p id="friend-msg" class="hidden text-gray-900 dark:text-white text-lg"></p>
<div class="flex flex-row space-x-4 w-full min-w-60 px-4 py-2">
<ul id="friends_list" class="bg-neutral-300 dark:bg-neutral-900 reverse-border space-y-2 hidden text-gray-900 dark:text-white overflow-scroll h-48 w-full">
</ul>
</div>
</div>
</div>
`;
}
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<Boolean> {
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);
}
}

View File

@ -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 `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">login.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 reverse-border flex flex-col items-center">
<form method="dialog" class="space-y-4">
<h1 class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome back ! please login.</h1>
<div class="flex flex-row justify-between space-x-4">
<input type="text" id="username" placeholder="username" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
</div>
<button id="login-button" type="submit" class="default-button w-full">login</button>
</form>
<p id="login-error-message" class="hidden text-red-700 dark:text-red-500"></p>
<hr class="my-4 w-64 reverse-border">
<div class="flex flex-col space-y-4 w-full">
<a target="_blank" id="login-google" class="default-button inline-flex items-center justify-center w-full">
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg" height=20 width=20 class="mr-2 justify-self-start" />
login with google
</a>
<a target="_blank" href="https://rusty.42angouleme.fr/issues/all" class="default-button inline-flex items-center justify-center w-full">
<img src="https://rusty.42angouleme.fr/assets/favicon-bb06adc80c8495db.ico" height=20 width=20 class="mr-2 justify-self-start" />
login with rusty
</a>
</div>
</div>
</div>
`;
}
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);
}
}

View File

@ -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() {
// <div class="text-center p-10 bg-white dark:bg-neutral-800 rounded-xl shadow space-y-4"-->
return `
<!--div class="default-border">
<div class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">knl_meowscendence</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center p-10 space-y-4 reverse-border">
<p class="text-gray-900 dark:text-white text-lg pb-4">i like pong</p>
<a class="default-button" href="/pong" data-link>
Pong
</a>
</div>
</div-->
`;
}
}

View File

@ -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 `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">pong_game.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div id="main-div" class="bg-neutral-200 dark:bg-neutral-800 text-center p-5 space-y-4 reverse-border">
<div id="player-inputs" class="flex flex-col space-y-4">
<h1 class="text-lg text-neutral-900 dark:text-white font-bold mt-2">enter the users ids/names</h1>
<div class="flex flex-row">
<span class="reverse-border w-full ml-2"><input type="text" id="player1" placeholder="Player 1" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input></span>
<span class="reverse-border w-full ml-2"><input type="text" id="player2" placeholder="Player 2" class="bg-white text-neutral-900 px-4 py-2 w-full input-border" required></input></span>
</div>
<button id="game-start" class="default-button">play</button>
</div>
<div id="game-buttons" class="hidden flex mt-4">
<button id="game-retry" class="default-button w-full mx-4 py-2">play again</button>
<a id="game-back" class="default-button w-full mx-4 py-2" href="/pong" data-link>back</a>
</div>
</div>
`;
}
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<string, boolean> = {};
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);
});
}
}

View File

@ -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 `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">pong_game.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<p class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome to pong!! Oo</p>
<div class="flex flex-col space-y-4">
<a class="default-button" href="/pong/local" data-link>
local match
</a>
<a class="default-button" href="/pong/tournament" data-link>
local tournament
</a>
</div>
</div>
</div>
`;
}
async run() {
dragElement(document.getElementById("window"));
}
}

View File

@ -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 `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">profile.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<div class="flex flex-col space-y-4 w-full">
<div id="profile-profile" class="default-border h-24 flex flex-row place-content-stretch content-center items-center space-x-6 pr-4">
</div>
<div class="flex flex-row space-x-4 w-full min-w-175">
<ul id="profile-pong-scorelist" class="reverse-border bg-neutral-300 dark:bg-neutral-900 h-48 w-full overflow-scroll no-scrollbar">
</ul>
<ul id="profile-tetris-scorelist" class="reverse-border bg-neutral-300 dark:bg-neutral-900 h-48 w-full overflow-scroll no-scrollbar">
</ul>
</div>
</div>
</div>
</div>
`;
}
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 = `
<div class="text-lg">Hi ${userdata.displayName} ! :D</div>
<div class="italic">${uuid}<div>
`;
nametag.classList.add("text-neutral-900", "dark:text-white");
const winrate = profile.appendChild(document.createElement("div"));
winrate.innerHTML = `
<div> total playcount: ${pc} </div>
<div> pong winrate: ${ (userdata.pong.wins == 0 && userdata.pong.losses == 0) ? "-" : Math.round(userdata.pong.wins / (userdata.pong.wins + userdata.pong.losses) * 100) + " %" } </div>
<div> tetris winrate: ${ (userdata.tetris.wins == 0 && userdata.tetris.losses == 0) ? "-" : Math.round(userdata.tetris.wins / (userdata.tetris.wins + userdata.tetris.losses) * 100) + " %" } </div>
`;
winrate.classList.add("text-neutral-900", "dark:text-white", "grow", "content-center");
}
}

View File

@ -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 `
<div id="main-window" class="default-border shadow-2x1 bg-neutral-200 dark:bg-neutral-800">
<div class="flex flex-row items-stretch">
<div class="inline-block bg-linear-to-b from-orange-200 to-orange-300 min-h-84 w-6 relative">
<div class="absolute bottom-1 left-full whitespace-nowrap origin-bottom-left -rotate-90 font-bold">knl_meowscendence</div>
</div>
<div class="flex flex-col items-center">
<div id="profile-items" class="flex flex-col items-center">
</div>
<div id="menu-bottom-div" class="hidden mt-auto flex flex-col items-center">
<hr class="my-2 w-32 reverse-border">
<button id="menu-logout" class="menu-default-button">logout</button>
</div>
</div>
</div>
</div>
`;
}
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 `
<a class="menu-default-button inline-flex items-center justify-center" href="/login" data-link>login</a>
<a class="menu-default-button inline-flex items-center justify-center" href="/register" data-link>register</a>
`;
}
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 `
<span class="menu-default-label inline-flex items-center justify-center">hi, ${ userdata.displayName.length > 8 ? userdata.displayName.substring(0, 8) + "." : userdata.displayName } !</span>
<hr class="my-2 w-32 reverse-border">
<a class="menu-default-button inline-flex items-center justify-center" href="/profile" data-link>profile</a>
<a class="menu-default-button inline-flex items-center justify-center" href="/settings" data-link>settings</a>
`;
}
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");
});
});
});
}
}

View File

@ -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 `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">register.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 reverse-border flex flex-col items-center">
<form method="dialog" class="space-y-4">
<h1 class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome ! please register.</h1>
<div class="flex flex-row justify-between space-x-4">
<input type="text" id="username" placeholder="username" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
</div>
<button id="register-button" type="submit" class="default-button w-full">register</button>
</form>
<p id="login-error-message" class="hidden text-red-700 dark:text-red-500 pt-4"></p>
<hr class="my-4 w-64 reverse-border">
<div class="flex flex-col space-y-4 w-full">
<a target="_blank" id="register-google" class="default-button inline-flex items-center justify-center w-full">
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg" height=20 width=20 class="mr-2 justify-self-start" />
register with google
</a>
<a target="_blank" href="https://rusty.42angouleme.fr/issues/all" class="default-button inline-flex items-center justify-center w-full">
<img src="https://rusty.42angouleme.fr/assets/favicon-bb06adc80c8495db.ico" height=20 width=20 class="mr-2 justify-self-start" />
register with rusty
</a>
</div>
</div>
</div>
`;
}
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);
}
}

View File

@ -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 `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">settings.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<div class="flex flex-row items-center place-items-center space-x-4">
<input type="text" id="displayName-input" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<button id="displayName-button" type="submit" class="default-button w-full">change display name</button>
</div>
<div id="upload" class="flex flex-row items-center place-items-center space-x-8">
<div id="upload-preview" class="hidden flex flex-col items-center place-items-center space-y-4">
<img id="upload-preview-img" class="w-20 h-20" />
<button id="upload-submit" type="submit" class="default-button">change avatar</button>
</div>
<label for="upload-file" class="default-button">select an avatar...</label><input type="file" id="upload-file" class="hidden" accept="image/*" />
</div>
<button id="deleteAccount-button" type="submit" class="default-button w-full">delete your account</button>
<div class="flex justify-center">
<hr class="w-50 reverse-border">
</div>
<button id="2fa-button" type="submit" class="default-button w-full">2fa</button>
</div>
</div>
`;
}
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")
}
});
}
}
}

View File

@ -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 `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">tetris_game.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div id="main-div" class="bg-neutral-200 dark:bg-neutral-800 text-center p-10 space-y-4 reverse-border">
<div class="flex flex-row justify-center items-start space-x-4">
<canvas id="hold" class="reverse-border" width="140" height="100"></canvas>
<canvas id="board" class="reverse-border" width="300" height="600"></canvas>
<canvas id="queue" class="reverse-border" width="140" height="420"></canvas>
</div>
<div id="game-buttons" class="hidden flex mt-4">
<button id="game-retry" class="default-button w-full mx-4 py-2">play again</button>
<a id="game-back" class="default-button w-full mx-4 py-2" href="/tetris" data-link>back</a>
</div>
</div>
</div>
`;
}
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<string, boolean> = {};
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");
}
}

View File

@ -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 `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">tetris_game.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 space-y-4 reverse-border">
<p class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome to tetris! :D</p>
<div class="flex flex-col space-y-4">
<a class="default-button" href="/tetris/solo" data-link>
solo game
</a>
<a class="default-button" href="/tetris/versus" data-link>
versus game
</a>
</div>
</div>
</div>
`;
}
async run() {
dragElement(document.getElementById("window"));
}
}

View File

@ -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 `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">tetris_game.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div id="main-div" class="bg-neutral-200 dark:bg-neutral-800 text-center p-5 pt-2 space-y-4 reverse-border">
<div id="player-inputs" class="flex flex-col space-y-4">
<h1 class="text-lg text-neutral-900 dark:text-white font-bold mt-2">enter the users ids/names</h1>
<div class="flex flex-row">
<span class="reverse-border w-full ml-2"><input type="text" id="player1" placeholder="Player 1" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input></span>
<span class="reverse-border w-full ml-2"><input type="text" id="player2" placeholder="Player 2" class="bg-white text-neutral-900 px-4 py-2 w-full input-border" required></input></span>
</div>
<button id="game-start" class="default-button">play</button>
</div>
<div id="game-boards" class="hidden flex flex-row justify-center items-start space-x-4">
<canvas id="board1-hold" class="reverse-border" width="140" height="100"></canvas>
<canvas id="board1-board" class="reverse-border" width="300" height="600"></canvas>
<canvas id="board1-queue" class="reverse-border" width="140" height="420"></canvas>
<canvas id="board2-hold" class="reverse-border" width="140" height="100"></canvas>
<canvas id="board2-board" class="reverse-border" width="300" height="600"></canvas>
<canvas id="board2-queue" class="reverse-border" width="140" height="420"></canvas>
</div>
<div id="game-buttons" class="hidden flex mt-4">
<button id="game-retry" class="default-button w-full mx-4 py-2">play again</button>
<a id="game-back" class="default-button w-full mx-4 py-2" href="/tetris" data-link>back</a>
</div>
</div>
</div>
`;
}
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<string, boolean> = {};
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);
});
}
}

View File

@ -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: <div class="select-all">${secret}</div>`;
secretText.classList.add("text-center")
const tokenInput = document.createElement("input");
tokenInput.type = "tel";
tokenInput.id = "totpPin";
tokenInput.name = "totpPin";
tokenInput.placeholder = "TOTP code";
tokenInput.required = true;
tokenInput.autocomplete = "off";
tokenInput.pattern = "[0-9]*";
tokenInput.setAttribute("inputmode", "numeric");
tokenInput.classList.add("bg-white", "text-neutral-900", "w-full", "px-4", "py-2", "input-border");
const tokenSubmit = document.createElement("button");
tokenSubmit.type = "submit";
tokenSubmit.classList.add("default-button", "w-full");
tokenSubmit.id = "totp-submit";
tokenSubmit.innerHTML = "submit";
const hr = document.createElement("hr");
hr.classList.add("my-2", "w-full", "reverse-border");
const t = document.createElement("h2");
t.innerHTML = "hey " + username +
` you are trying to add 2fa</br>
just add the following to your app and enter the code bellow ↓
`;
t.classList.add("text-center")
document.getElementById("app")?.appendChild(popup);
const form = document.createElement("form");
form.method = "dialog";
form.classList.add("space-y-4");
form.appendChild(tokenInput);
form.appendChild(tokenSubmit);
popup_content.appendChild(t)
popup_content.appendChild(qrDivTOTP);
popup_content.appendChild(secretText);
popup_content.appendChild(hr)
popup_content.appendChild(form);
dragElement(document.getElementById("2fa-enable-popup"));
document.getElementById("totp-submit")?.addEventListener("click", totpVerify)
}

View File

@ -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 `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">pong_game.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div id="main-div" class="bg-neutral-200 dark:bg-neutral-800 text-center p-10 pt-5 space-y-4 reverse-border">
<div id="tournament-id">
<p class="text-neutral-900 dark:text-white text-lg font-bold pb-4">how many players ?</p>
<div class="flex flex-col space-y-4">
<select id="playerNumber" class="bg-white text-shadow-neutral-900 p-2 input-border">
<option value="">-- player number --</option>
<option value="2">2 players</option>
<option value="3">3 players</option>
<option value="4">4 players</option>
<option value="6">6 players</option>
<option value="8">8 players</option>
</select>
<button type="submit" id="bracket-generate" class="default-button">create the bracket</button>
<div id="bracket" class="flex flex-col space-y-6 items-center"></div>
</div>
</div>
<div id="announcement" class="hidden flex flex-col space-y-8">
<div id="bracket-announcement" class="flex flex-col space-y-6 items-center">
</div>
<span id="announcement-text" class="text-lg font-bold text-neutral-900 dark:text-white"></span>
<button type="submit" id="tournament-continue" class="default-button">let's go</button>
</div>
<div id="winner-div" class="hidden flex flex-col items-center space-y-8">
<img src="https://api.kanel.ovh/pp?id=3" class="w-25 h-25 default-border" \>
<span id="winner-text" class="text-2x1 text-neutral-900 dark:text-white"></span>
</div>
</div>
</div>
`;
}
async runGame(p1_id: number, p2_id: number, players: string[]): Promise<number> {
return new Promise<number>(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<string, boolean> = {};
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<void> {
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);
});
}
}

View File

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

View File

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

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

View File

@ -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,

View File

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

View File

@ -0,0 +1,19 @@
import axios from 'axios'
/**
* @param {string} username
* @param {import('fastify').FastifyInstance} fastify
*/
export async function authUserRemove(username, fastify) {
const url = (process.env.USER_URL || "http://localhost:3002") + "/users/" + username;
const cookie = fastify.jwt.sign({ user: "admin" });
await axios.delete(
url,
{
headers: {
'Cookie': 'token=' + cookie,
},
}
);
}

View File

@ -29,7 +29,7 @@ async function loadContract() {
async function callGetScore(id) {
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;

View File

@ -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: [],
}