From 0ea80ed6381290072dd1cd48e608a32164aa9174 Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Fri, 5 Sep 2025 23:04:44 +0200 Subject: [PATCH 01/45] WIP API, match history done --- src/api/user/TODO | 39 ++++++++++++ src/api/user/default.js | 138 ++++++++++++++++++++++++++++++++-------- 2 files changed, 151 insertions(+), 26 deletions(-) create mode 100644 src/api/user/TODO diff --git a/src/api/user/TODO b/src/api/user/TODO new file mode 100644 index 0000000..83a6f8c --- /dev/null +++ b/src/api/user/TODO @@ -0,0 +1,39 @@ +Todo : +- crate a whole image upload API that ensures files are not executables, converts to a single type, stores the image and returns a UID to address them +- create users with all the necessary arguments (assign avatar randomly if none is provided) +- add endpoints to return number of friends and matches +- use more schema in endpoints ? instead of using many checks everywhere +- split code into files with functions called in the endpoints +- test everything (using Postman) + + + +POST user : +- uploading the avatar involves annoying file handling functions +- avatar must be chosen randomly if not provided + +GET friends : +- should also work with indexes ideally (like GET matchHistory) + +POST friends : +- rework to make work more similarly to POST matchHistory ? + +PATCH : +- changing the avatar involves annoying file handling functions + +DELETE : +- what can be deleted ? +-> users +-> friends +-> user info ? like display name, avatar, or should they just be changeable ? +-> match history ? does it need to be deletable to comply with RGPD ? + + + +Known issues : +- When game ends we must ensure only one match result is written to the blockchain -> not an issue if we do the server-side as the server can make the single post, but if it is client-side we must take care not to send two (either by creating an API for the game that will have the necessary protections or by adding these protections directly into the user API) +-> Right now POST matchHistory will send the two matches to the blockchain API + +-> Users set to private should not appear in the friends lists of other public users + +-> Right now the client can only get his own friends. Do we not want any other client to be able to see his friends ? diff --git a/src/api/user/default.js b/src/api/user/default.js index a5efe65..d95fa8e 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -1,23 +1,25 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; import Database from 'better-sqlite3'; +import fs from 'fs'; -var env = process.env.NODE_ENV || 'development'; - -let database; +const env = process.env.NODE_ENV || 'development'; if (!env || env === 'development') { - database = new Database(":memory:", { verbose: console.log }); + const database = new Database(":memory:", { verbose: console.log }); } else { - var dbPath = process.env.DB_PATH || '/db/db.sqlite' - database = new Database(dbPath); + const dbPath = process.env.DB_PATH || '/db/db.sqlite' + const database = new Database(dbPath); } function prepareDB() { database.exec(` CREATE TABLE IF NOT EXISTS userData ( username TEXT PRIMARY KEY, - displayName TEXT + displayName TEXT, + avatar BLOB, + wins INTEGER, + losses INTEGER ) STRICT `); database.exec(` @@ -26,30 +28,39 @@ function prepareDB() { friendName TEXT, UNIQUE(username, friendName), CHECK(username != friendName) - ) + ) STRICT + `); + database.exec(` + CREATE TABLE IF NOT EXISTS matchHistory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT, + matchId INTEGER + ) STRICT `); } prepareDB(); // POST -const createUser = database.prepare('INSERT INTO userData (username, displayName) VALUES (?, ?);'); +const createUser = database.prepare('INSERT INTO userData (username, displayName, avatar, wins, losses) VALUES (?, ?, ?, 0, 0);'); const addFriend = database.prepare('INSERT INTO friends (username, friendName) VALUES (?, ?);'); +const addMatch = database.prepare('INSERT INTO matchHistory (username, matchId) VALUES (?, ?);'); // PATCH const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;'); +const changeAvatar = database.prepare('UPDATE userData SET avatar = ? WHERE username = ?;'); // GET -const getUserInfo = database.prepare('SELECT * FROM userData WHERE username = ?;'); const getUserData = database.prepare('SELECT * FROM userData;'); +const getUserInfo = database.prepare('SELECT * FROM userData WHERE username = ?;'); const getFriends = database.prepare('SELECT friendName FROM friends WHERE username = ?;'); -// const isFriend = database.prepare('SELECT 1 FROM friends WHERE username = ? AND friendName = ?;'); +const getMatchHistory = database.prepare('SELECT matchId FROM matchHistory WHERE username = ? AND id BETWEEN ? AND ? ORDER BY id ASC;'); // 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 username = ?;'); /** * @param {import('fastify').FastifyInstance} fastify @@ -88,7 +99,6 @@ export default async function(fastify, options) { 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); @@ -98,7 +108,6 @@ export default async function(fastify, options) { 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); @@ -108,14 +117,11 @@ export default async function(fastify, options) { 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:" }); } @@ -126,22 +132,64 @@ export default async function(fastify, options) { return reply.code(500).send({ error: "Internal server error" }); } }); + fastify.get('/users/:userId/matchHistory', { 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') { + if (!matchHistory) { + return reply.code(404).send({ error: "User has not participated in any matches yet" }); + } + if (!request.body || !request.body.i_start || !request.body.i_end) { + return reply.code(400).send({ error: "Please specify both a strting and an ending index" }); + } + if (request.body.i_end < request.body.i_start) { + return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); + } + const matchHistoryId = getMatchHistory.all(userId, request.body.i_start, request.body.i_end - 1); + const promises = matchHistoryId.map(async (id) => { + const res = await fetch('/' + userId, { method: "GET", headers: { "Content-Type": "application/json" } }); + if (!res.ok) + throw new Error('Failed to fetch item ${id}'); + return res.json(); + }); + 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" }); + } + ); // POST fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => { 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" }); } - createUser.run(userId, userId); + if (!request.body || !request.body.displayName) { + return reply.code(400).send({ error: "Please specify a display name and an avatar" }); + } + const avatar; + if (request.body.avatar) { + avatar = request.body.avatar; + } else { + avatar = 1;// randomly chosen avatar + } + createUser.run(userId, request.body.displayName, avatar); 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; @@ -167,6 +215,37 @@ export default async function(fastify, options) { return reply.code(500).send({ error: "Internal server error" }); } }); + fastify.post('/users/:userId/matchHistory', { 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 || !request.body.p1Score || !request.body.p2Score) { + return reply.code(400).send({ error: "Please specify the second player and the score of both players" }); + } + 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: "Second player does not exist" }); + } + if (request.body.user === userId) { + return reply.code(400).send({ error: "Do you have dementia ? You cannot have played a match against yourself, gramps" }); + } + if (request.body.p1Score < 0 || request.body.p2Score < 0) { + return reply.code(400).send({ error: "A score cannot be strictly negative" }); + } + const res = await fetch('/', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ p1: userId, p2: request.body.user, p1Score: request.body.p1Score, p2Score: request.body.p2Score }) }); + if (!res.ok) + return reply.code(500).send({ error: "Internal server error" }); + addMatch.run(userId, res.id); + return reply.code(200).send({ msg: "Match history retrieved successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } + }); // PATCH fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate] }, async (request, reply) => { @@ -179,21 +258,28 @@ export default async function(fastify, options) { 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(200).send({ msg: "Display name modified sucessfully" }); + } + if (member === 'avatar') { + if (!request.body || !request.body.avatar) { + return reply.code(400).send({ error: "Please specify an avatar" }); + } + changeAvatar.run(request.body.avatar, userId); + return reply.code(200).send({ msg: "Avatar modified sucessfully" }); + } + return reply.code(400).send({ error: "Avatar does not exist" }) } return reply.code(400).send({ error: "Member does not exist" }) } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); } - }) + }); // DELETE /** @@ -218,9 +304,9 @@ export default async function(fastify, options) { 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(200).send({ msg: "Display name cleared sucessfully" }); } - return reply.code(400).send({ msg: "member does not exist" }) + 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' }); } From 62d4c7df6e236bfb24f73c36ec2daaa5f100bb57 Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Mon, 29 Sep 2025 19:43:13 +0200 Subject: [PATCH 02/45] added doc --- doc/user/user.md | 408 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 doc/user/user.md diff --git a/doc/user/user.md b/doc/user/user.md new file mode 100644 index 0000000..cdc5779 --- /dev/null +++ b/doc/user/user.md @@ -0,0 +1,408 @@ +# User + +Available endpoints: +- POST `/users/:userId` +- POST `/users/:userId/friends` +- POST `/users/:userId/matchHistory` +- GET `/users` +- GET `/users/:userId` +- GET `/users/:userId/friends` +- GET `/users/:userId/matchHistory` +- PATCH `/users/:userId/:member` +- DELETE `/users/:userId` +- DELETE `/users/:userId/:member` +- DELETE `/users/:userId/friends` +- DELETE `/users/:userId/friends/:friendId` +- DELETE `/users/:userId/matchHistory` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/users/:userId` + +Used to create an user + +Input needed : +```json +{ + "displayName": "" +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "User created successfully" +} +``` +- 400 with response (if no user is specified in header, or user already exists, or no display name is specified in body) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is not admin) +```json +{ + "error": "" +} +``` + +## POST `/users/:userId/friends` + +Used to add a friend + +Input needed : +```json +{ + "friend": "" +} +``` + +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 no friend is specified in body) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or friend does not exist) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` + +## POST `/users/:userId/matchHistory` + +Used to add a match result + +Input needed : +```json +{ + "opponent": "", + "p1Score": , + "p2Score": +} +``` + +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) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or opponent does not exist) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` + +## GET `/users` + +Used to get the user list + +Always returns: +- 200 with response (list of user objects) +```json +[ + { + "username": "", + "displayName": "", + "wins": , + "losses": + }, + ... +] +``` + +## GET `/users/:userId` + +Used to get an user + +Can return: +- 200 with response (an user object) +```json +{ + "username": "", + "displayName": "", + "wins": , + "losses": +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/friends` + +Used to the friends of a user + +Can return: +- 200 with response (list of friend objects) +```json +[ + { + "friendName": "" + }, + ... +] +``` +- 404 with response (if user does not exist, or user does not have friends) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/matchHistory` + +Used to the match history of a user + +Input needed : +```json +{ + "iStart": , + "iEnd": +} +``` + +Can return: +- 200 with response (list of matches results (between iStart and iEnd)) +```json +[ + { + "score": + { + "p1": "", + "p2": "", + "p1Score": "", + "p2Score": "" + }, + "tx": "" + }, + ... +] +``` +- 400 with response (if iStart/iEnd does not exist, or iEnd < iStart) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or user did not play any matches) +```json +{ + "error": "" +} +``` + +## PATCH `/users/:userId/:member` + +Used to modify the member of a user (only displayName can be modified) + +Input needed : +```json +{ + "": "" +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "<:member> modified sucessfully" +} +``` +- 400 with response (if no user is specified in header, or new value of member to modify is not provided in the body, or member does not exist) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is not admin) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId` + +Used to delete a user + +Can return: +- 200 with response +```json +{ + "msg": "User deleted successfully" +} +``` +- 404 with response (user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/:member` + +Used to delete a member (only displayName can be deleted) + +Can return: +- 200 with response +```json +{ + "msg": "<:member> deleted successfully" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 400 with response (if no user is specified in header, or member to delete does not exist) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/friends` + +Used to delete friends + +Can return: +- 200 with response +```json +{ + "msg": "Friends deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/friends/:friendId` + +Used to delete a friend + +Can return: +- 200 with response +```json +{ + "msg": "Friend deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or friend does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/matchHistory` + +Used to delete the match history + +Can return: +- 200 with response +```json +{ + "msg": "Match history deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` From 22da9af53d970d3a9b59055a5ddb77a21595b6f8 Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Thu, 2 Oct 2025 11:21:42 +0200 Subject: [PATCH 03/45] improved user API, see doc --- doc/user/user.md | 150 ++++++++++++++------ src/api/user/TODO | 45 ++---- src/api/user/default.js | 304 +++++++++++++++++++++++++++------------- 3 files changed, 326 insertions(+), 173 deletions(-) diff --git a/doc/user/user.md b/doc/user/user.md index cdc5779..3c18841 100644 --- a/doc/user/user.md +++ b/doc/user/user.md @@ -5,9 +5,12 @@ Available endpoints: - POST `/users/:userId/friends` - POST `/users/:userId/matchHistory` - GET `/users` +- GET `/users/count` - GET `/users/:userId` - GET `/users/:userId/friends` +- GET `/users/:userId/friends/count` - GET `/users/:userId/matchHistory` +- GET `/users/:userId/matchHistory/count` - PATCH `/users/:userId/:member` - DELETE `/users/:userId` - DELETE `/users/:userId/:member` @@ -54,16 +57,9 @@ Can return: } ``` -## POST `/users/:userId/friends` +## POST `/users/:userId/friends/:friendId` -Used to add a friend - -Input needed : -```json -{ - "friend": "" -} -``` +Used to add a friend to an user Can return: - 200 with response @@ -72,7 +68,7 @@ Can return: "msg": "Friend added successfully" } ``` -- 400 with response (if no user is specified in header, or friend is the user specified in header, or no friend is specified in body) +- 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": "" @@ -93,7 +89,7 @@ Can return: ## POST `/users/:userId/matchHistory` -Used to add a match result +Used to add a match result to an user Input needed : ```json @@ -130,22 +126,49 @@ Can return: } ``` -## GET `/users` +## GET `/users?iStart=&iEnd=` -Used to get the user list +Used to get the list of users + +Can return: +- 200 with response (list of user objects (between iStart and iEnd)) +```json +{ + "users": + [ + { + "username": "", + "displayName": "", + "wins": , + "losses": + }, + ... + ] +} +``` +- 400 with response (if iStart/iEnd is missing, or iEnd < iStart) +```json +{ + "error": "" +} +``` +- 404 with response (if no users exist in the selected range) +```json +{ + "error": "" +} +``` + +## GET `/users/count` + +Used to get the number of users Always returns: -- 200 with response (list of user objects) +- 200 with response ```json -[ - { - "username": "", - "displayName": "", - "wins": , - "losses": - }, - ... -] +{ + "n_": +} ``` ## GET `/users/:userId` @@ -169,38 +192,57 @@ Can return: } ``` -## GET `/users/:userId/friends` +## GET `/users/:userId/friends?iStart=&iEnd=` -Used to the friends of a user +Used to get the friends of an user Can return: -- 200 with response (list of friend objects) +- 200 with response (list of friend objects (between iStart and iEnd)) ```json -[ - { - "friendName": "" - }, - ... -] +{ + "friends": + [ + { + "friendName": "" + }, + ... + ] +} ``` -- 404 with response (if user does not exist, or user does not have friends) +- 400 with response (if iStart/iEnd is missing, or iEnd < iStart) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or no friends exist in the selected range) ```json { "error": "" } ``` -## GET `/users/:userId/matchHistory` +## GET `/users/:userId/friends/count` -Used to the match history of a user +Used to get the number of friends of an user -Input needed : +Can return: +- 200 with response ```json { - "iStart": , - "iEnd": + "n_": } ``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/matchHistory?iStart=&iEnd=` + +Used to get the match history of an user Can return: - 200 with response (list of matches results (between iStart and iEnd)) @@ -225,7 +267,25 @@ Can return: "error": "" } ``` -- 404 with response (if user does not exist, or user did not play any matches) +- 404 with response (if user does not exist, or no matches exist in the selected range) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/matchHistory/count` + +Used to get the number of matches an user played + +Can return: +- 200 with response +```json +{ + "n_": +} +``` +- 404 with response (if user does not exist) ```json { "error": "" @@ -234,7 +294,7 @@ Can return: ## PATCH `/users/:userId/:member` -Used to modify the member of a user (only displayName can be modified) +Used to modify a member of an user (only displayName can be modified) Input needed : ```json @@ -271,7 +331,7 @@ Can return: ## DELETE `/users/:userId` -Used to delete a user +Used to delete an user Can return: - 200 with response @@ -289,7 +349,7 @@ Can return: ## DELETE `/users/:userId/:member` -Used to delete a member (only displayName can be deleted) +Used to delete a member of an user (only displayName can be deleted) Can return: - 200 with response @@ -319,7 +379,7 @@ Can return: ## DELETE `/users/:userId/friends` -Used to delete friends +Used to delete the friends of an user Can return: - 200 with response @@ -349,7 +409,7 @@ Can return: ## DELETE `/users/:userId/friends/:friendId` -Used to delete a friend +Used to delete a friend of an user Can return: - 200 with response @@ -379,7 +439,7 @@ Can return: ## DELETE `/users/:userId/matchHistory` -Used to delete the match history +Used to delete the match history of an user Can return: - 200 with response diff --git a/src/api/user/TODO b/src/api/user/TODO index 83a6f8c..653d4f3 100644 --- a/src/api/user/TODO +++ b/src/api/user/TODO @@ -1,39 +1,16 @@ Todo : -- crate a whole image upload API that ensures files are not executables, converts to a single type, stores the image and returns a UID to address them -- create users with all the necessary arguments (assign avatar randomly if none is provided) -- add endpoints to return number of friends and matches -- use more schema in endpoints ? instead of using many checks everywhere +- test matchHistory & wins/losses + +- create users with an avatar (by default) -> POST/GET/PATCH/DELETE avatar +- create a whole image upload API that ensures files are not executables, converts to a single type, stores the image and returns a UID to address them +- add a privacy setting so not anybody can GET friends, match history, etc. ? + + + +- choose where to use == and === +- use more schema in endpoints for querystring and body - split code into files with functions called in the endpoints -- test everything (using Postman) -POST user : -- uploading the avatar involves annoying file handling functions -- avatar must be chosen randomly if not provided - -GET friends : -- should also work with indexes ideally (like GET matchHistory) - -POST friends : -- rework to make work more similarly to POST matchHistory ? - -PATCH : -- changing the avatar involves annoying file handling functions - -DELETE : -- what can be deleted ? --> users --> friends --> user info ? like display name, avatar, or should they just be changeable ? --> match history ? does it need to be deletable to comply with RGPD ? - - - -Known issues : -- When game ends we must ensure only one match result is written to the blockchain -> not an issue if we do the server-side as the server can make the single post, but if it is client-side we must take care not to send two (either by creating an API for the game that will have the necessary protections or by adding these protections directly into the user API) --> Right now POST matchHistory will send the two matches to the blockchain API - --> Users set to private should not appear in the friends lists of other public users - --> Right now the client can only get his own friends. Do we not want any other client to be able to see his friends ? +Always update API doc diff --git a/src/api/user/default.js b/src/api/user/default.js index d95fa8e..ef9cf43 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -1,29 +1,33 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; import Database from 'better-sqlite3'; -import fs from 'fs'; const env = process.env.NODE_ENV || 'development'; +let database; if (!env || env === 'development') { - const database = new Database(":memory:", { verbose: console.log }); + database = new Database(':memory:', { verbose: console.log }); } else { const dbPath = process.env.DB_PATH || '/db/db.sqlite' - const database = new Database(dbPath); + database = new Database(dbPath); } function prepareDB() { database.exec(` CREATE TABLE IF NOT EXISTS userData ( - username TEXT PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT, displayName TEXT, - avatar BLOB, wins INTEGER, - losses INTEGER + losses INTEGER, + UNIQUE(username), + CHECK(wins >= 0), + CHECK(losses >= 0) ) STRICT `); database.exec(` CREATE TABLE IF NOT EXISTS friends ( + id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, friendName TEXT, UNIQUE(username, friendName), @@ -42,25 +46,31 @@ function prepareDB() { prepareDB(); // POST -const createUser = database.prepare('INSERT INTO userData (username, displayName, avatar, wins, losses) VALUES (?, ?, ?, 0, 0);'); +const createUser = database.prepare('INSERT INTO userData (username, displayName, wins, losses) VALUES (?, ?, 0, 0);'); const addFriend = database.prepare('INSERT INTO friends (username, friendName) VALUES (?, ?);'); const addMatch = database.prepare('INSERT INTO matchHistory (username, matchId) VALUES (?, ?);'); +const incWins = database.prepare('UPDATE userData SET wins = wins + 1 WHERE username = ?;'); +const incLosses = database.prepare('UPDATE userData SET losses = losses + 1 WHERE username = ?'); // PATCH const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;'); -const changeAvatar = database.prepare('UPDATE userData SET avatar = ? WHERE username = ?;'); // GET -const getUserData = database.prepare('SELECT * FROM userData;'); -const getUserInfo = database.prepare('SELECT * FROM userData WHERE username = ?;'); -const getFriends = database.prepare('SELECT friendName FROM friends WHERE username = ?;'); -const getMatchHistory = database.prepare('SELECT matchId FROM matchHistory WHERE username = ? AND id BETWEEN ? AND ? ORDER BY id ASC;'); +const getUserData = database.prepare('SELECT username, displayName, wins, losses FROM userData LIMIT ? OFFSET ?;'); +const getUserInfo = database.prepare('SELECT username, displayName, wins, losses 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 FROM matchHistory WHERE username = ? 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 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 username = ?;'); +const deleteStats = database.prepare('UPDATE userData SET wins = 0, losses = 0 WHERE username = ?;'); /** * @param {import('fastify').FastifyInstance} fastify @@ -75,7 +85,7 @@ export default async function(fastify, options) { }); fastify.register(fastifyCookie); - fastify.decorate("authenticate", async function(request, reply) { + fastify.decorate('authenticate', async function(request, reply) { try { const jwt = await request.jwtVerify(); request.user = jwt.user; @@ -84,11 +94,11 @@ 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'); } } catch (err) { reply.code(401).send({ error: 'Unauthorized' }); @@ -98,17 +108,39 @@ export default async function(fastify, options) { // GET fastify.get('/users', { preHandler: [fastify.authenticate] }, async (request, reply) => { try { - const users = getUserData.all(); + const { iStart, iEnd } = request.query; + if (!iStart || !iEnd) { + return reply.code(400).send({ error: "Please specify both a starting and an ending index" }); + } + 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" }); + } return reply.code(200).send({ users }); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); } }); + fastify.get('/users/count', { preHandler: [fastify.authenticate] }, async (request, reply) => { + 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" }); + } + }); fastify.get('/users/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => { try { - const info = getUserInfo.get(request.params.userId); - return reply.code(200).send({ info }); + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const userInfo = getUserInfo.get(request.params.userId); + return reply.code(200).send({ userInfo }); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); @@ -120,13 +152,31 @@ export default async function(fastify, options) { 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 }); + const { iStart, iEnd } = request.query; + if (!iStart || !iEnd) { + return reply.code(400).send({ error: "Please specify both a starting and an ending index" }); } + if (Number(iEnd) < Number(iStart)) { + return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); + } + const friends = getFriends.all(userId, Number(iEnd) - Number(iStart), Number(iStart)); + if (!friends.length) { + return reply.code(404).send({ error: "No friends exist in the selected range" }); + } + 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/count', { 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" }); + } + 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" }); @@ -138,78 +188,91 @@ export default async function(fastify, options) { if (!getUserInfo.get(userId)) { return reply.code(404).send({ error: "User does not exist" }); } - if (userId == request.user || request.user == 'admin') { - if (!matchHistory) { - return reply.code(404).send({ error: "User has not participated in any matches yet" }); - } - if (!request.body || !request.body.i_start || !request.body.i_end) { - return reply.code(400).send({ error: "Please specify both a strting and an ending index" }); - } - if (request.body.i_end < request.body.i_start) { - return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); - } - const matchHistoryId = getMatchHistory.all(userId, request.body.i_start, request.body.i_end - 1); - const promises = matchHistoryId.map(async (id) => { - const res = await fetch('/' + userId, { method: "GET", headers: { "Content-Type": "application/json" } }); - if (!res.ok) - throw new Error('Failed to fetch item ${id}'); - return res.json(); - }); - const matchHistory = await Promise.all(promises); - return reply.code(200).send({ matchHistory }); + const { iStart, iEnd } = request.query; + if (!iStart || !iEnd) { + return reply.code(400).send({ error: "Please specify both a starting and an ending index" }); } + if (Number(iEnd) < Number(iStart)) { + return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); + } + const matchHistoryId = getMatchHistory.all(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 (id) => { + const res = await fetch('https://transcendence-api-scoreStore:3000/' + id, { method: "GET", headers: { "Content-Type": "application/json" } }); + if (!res.ok) + throw new Error('Failed to fetch item from blockchain API'); + return res.json(); + }); + 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" }); } - ); + }); + fastify.get('/users/:userId/matchHistory/count', { 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" }); + } + const row = getNumberMatches.get(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" }); + } + }); // POST fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => { try { const userId = request.params.userId; - if (request.user != 'admin') { + if (!request.user || !request.user.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + if (request.user.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 and an avatar" }); + return reply.code(400).send({ error: "Please specify a display name" }); } - const avatar; - if (request.body.avatar) { - avatar = request.body.avatar; - } else { - avatar = 1;// randomly chosen avatar - } - createUser.run(userId, request.body.displayName, avatar); - return reply.code(200).send({ msg: "User created sucessfully" }); + 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" }); } }); - fastify.post('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => { + fastify.post('/users/:userId/friends/:friendId', { 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) { + if (!request.user) { return reply.code(400).send({ error: "Please specify a user" }); } + 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" }); } - if (!getUserInfo.get(request.body.user)) { + const friendId = request.params.friendId; + if (!getUserInfo.get(friendId)) { return reply.code(404).send({ error: "Friend does not exist" }); } - if (request.body.user === userId) { + if (friendId === 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" }); + 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" }); @@ -218,29 +281,37 @@ export default async function(fastify, options) { fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate] }, async (request, reply) => { try { const userId = request.params.userId; - if (request.user != 'admin' && request.user != userId) { + if (!request.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + if (request.user !== 'admin' && request.user !== userId) { return reply.code(401).send({ error: "Unauthorized" }); } - if (!request.body || !request.body.user || !request.body.p1Score || !request.body.p2Score) { - return reply.code(400).send({ error: "Please specify the second player and the score of both players" }); + if (!request.body || !request.body.opponent || !request.body.p1Score || !request.body.p2Score) { + return reply.code(400).send({ error: "Please specify the opponent and the score of both players" }); } 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: "Second player does not exist" }); + if (!getUserInfo.get(request.body.opponent)) { + return reply.code(404).send({ error: "Opponent does not exist" }); } - if (request.body.user === userId) { - return reply.code(400).send({ error: "Do you have dementia ? You cannot have played a match against yourself, gramps" }); + if (request.body.opponent === userId) { + return reply.code(400).send({ error: "Do you have dementia ? You cannot have played a match against yourself gramps" }); } if (request.body.p1Score < 0 || request.body.p2Score < 0) { return reply.code(400).send({ error: "A score cannot be strictly negative" }); } - const res = await fetch('/', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ p1: userId, p2: request.body.user, p1Score: request.body.p1Score, p2Score: request.body.p2Score }) }); + const res = await fetch('http://localhost:3003/', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ p1: userId, p2: request.body.opponent, p1Score: request.body.p1Score, p2Score: request.body.p2Score }) }); if (!res.ok) return reply.code(500).send({ error: "Internal server error" }); addMatch.run(userId, res.id); - return reply.code(200).send({ msg: "Match history retrieved successfully" }); + if (request.body.p1Score > request.body.p2Score) { + incWins.run(userId); + } else if (request.body.p1Score < request.body.p2Score) { + incLosses.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" }); @@ -251,7 +322,10 @@ export default async function(fastify, options) { fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate] }, async (request, reply) => { try { const userId = request.params.userId; - if (request.user != 'admin' && request.user != userId) { + if (!request.user || !request.user.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + if (request.user.user !== 'admin' && request.user.user !== userId) { return reply.code(401).send({ error: "Unauthorized" }); } if (!getUserInfo.get(userId)) { @@ -263,16 +337,7 @@ export default async function(fastify, options) { 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 sucessfully" }); - } - if (member === 'avatar') { - if (!request.body || !request.body.avatar) { - return reply.code(400).send({ error: "Please specify an avatar" }); - } - changeAvatar.run(request.body.avatar, userId); - return reply.code(200).send({ msg: "Avatar modified sucessfully" }); - } - return reply.code(400).send({ error: "Avatar does not exist" }) + return reply.code(200).send({ msg: "Display name modified successfully" }); } return reply.code(400).send({ error: "Member does not exist" }) } catch (err) { @@ -282,16 +347,15 @@ export default async function(fastify, options) { }); // 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); + deleteMatchHistory.run(request.params.userId); deleteFriends.run(request.params.userId); + deleteUser.run(request.params.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" }); @@ -299,12 +363,19 @@ export default async function(fastify, options) { }); fastify.delete('/users/:userId/:member', { preHandler: fastify.authenticate }, async (request, reply) => { try { - const user = request.user; + if (!request.user || !request.user.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const user = request.user.user; const member = request.params.member; if (user == 'admin' || user == request.params.userId) { - if (member == 'displayName') { + if (member === 'displayName') { changeDisplayName.run("", request.params.userId); - return reply.code(200).send({ msg: "Display name cleared sucessfully" }); + return reply.code(200).send({ msg: "Display name deleted successfully" }); } return reply.code(400).send({ msg: "Member does not exist" }) } else { @@ -316,18 +387,63 @@ export default async function(fastify, options) { } }); - fastify.delete('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { + fastify.delete('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => { try { + if (!request.user || !request.user.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } 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) { + if (request.user.user != 'admin' && request.user.user != userId) { return reply.code(401).send({ error: "Unauthorized" }); } + 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" }); + } + }); + fastify.delete('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { + try { + if (!request.user || !request.user.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + if (request.user.user != 'admin' && request.user.user != userId) { + return reply.code(401).send({ error: "Unauthorized" }); + } + const friendId = request.params.friendId; + if (!getFriend.get(friendId)) { + return reply.code(404).send({ error: "Friend does not exist" }); + } deleteFriend.run(userId, friendId); - return reply.code(200).send({ msg: "Friend remove sucessfully" }); + return reply.code(200).send({ msg: "Friend deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } + }); + fastify.delete('/users/:userId/matchHistory', { preHandler: [fastify.authenticate] }, async (request, reply) => { + try { + if (!request.user || !request.user.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + if (request.user.user != 'admin' && request.user.user != userId) { + return reply.code(401).send({ error: "Unauthorized" }); + } + deleteMatchHistory.run(userId); + deleteStats.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" }); From 439e5a0acfd8d18c2694e0f5f89982d1e55013bc Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Thu, 2 Oct 2025 16:04:50 +0200 Subject: [PATCH 04/45] finished, functional API --- src/api/user/TODO | 10 +- src/api/user/dFriend.js | 23 +++ src/api/user/dFriends.js | 19 ++ src/api/user/dMatchHistory.js | 20 ++ src/api/user/dMember.js | 25 +++ src/api/user/dUser.js | 14 ++ src/api/user/default.js | 353 ++++----------------------------- src/api/user/gFriends.js | 20 ++ src/api/user/gMatchHistory.js | 29 +++ src/api/user/gNumberFriends.js | 13 ++ src/api/user/gNumberMatches.js | 13 ++ src/api/user/gNumberUsers.js | 9 + src/api/user/gUser.js | 13 ++ src/api/user/gUsers.js | 16 ++ src/api/user/pFriend.js | 29 +++ src/api/user/pMatchHistory.js | 36 ++++ src/api/user/pUser.js | 22 ++ src/api/user/uMember.js | 26 +++ 18 files changed, 368 insertions(+), 322 deletions(-) create mode 100644 src/api/user/dFriend.js create mode 100644 src/api/user/dFriends.js create mode 100644 src/api/user/dMatchHistory.js create mode 100644 src/api/user/dMember.js create mode 100644 src/api/user/dUser.js create mode 100644 src/api/user/gFriends.js create mode 100644 src/api/user/gMatchHistory.js create mode 100644 src/api/user/gNumberFriends.js create mode 100644 src/api/user/gNumberMatches.js create mode 100644 src/api/user/gNumberUsers.js create mode 100644 src/api/user/gUser.js create mode 100644 src/api/user/gUsers.js create mode 100644 src/api/user/pFriend.js create mode 100644 src/api/user/pMatchHistory.js create mode 100644 src/api/user/pUser.js create mode 100644 src/api/user/uMember.js diff --git a/src/api/user/TODO b/src/api/user/TODO index 653d4f3..326da16 100644 --- a/src/api/user/TODO +++ b/src/api/user/TODO @@ -1,15 +1,7 @@ Todo : -- test matchHistory & wins/losses - - create users with an avatar (by default) -> POST/GET/PATCH/DELETE avatar - create a whole image upload API that ensures files are not executables, converts to a single type, stores the image and returns a UID to address them -- add a privacy setting so not anybody can GET friends, match history, etc. ? - - - -- choose where to use == and === -- use more schema in endpoints for querystring and body -- split code into files with functions called in the endpoints +- add a privacy setting so not anybody can GET friends, match history, etc. (what are the RGPD requirements ?) ? diff --git a/src/api/user/dFriend.js b/src/api/user/dFriend.js new file mode 100644 index 0000000..333bd93 --- /dev/null +++ b/src/api/user/dFriend.js @@ -0,0 +1,23 @@ +export async function dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend) { + try { + if (!request.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + if (request.user !== 'admin' && request.user !== userId) { + return reply.code(401).send({ error: "Unauthorized" }); + } + const friendId = request.params.friendId; + if (!getFriend.get(userId, friendId)) { + return reply.code(404).send({ error: "Friend does not exist" }); + } + deleteFriend.run(userId, friendId); + return reply.code(200).send({ msg: "Friend deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dFriends.js b/src/api/user/dFriends.js new file mode 100644 index 0000000..803dd89 --- /dev/null +++ b/src/api/user/dFriends.js @@ -0,0 +1,19 @@ +export async function dFriends(request, reply, fastify, getUserInfo, deleteFriends) { + try { + if (!request.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + const userId = request.params.userId; + 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" }); + } + deleteFriends.run(userId); + return reply.code(200).send({ msg: "Friends deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dMatchHistory.js b/src/api/user/dMatchHistory.js new file mode 100644 index 0000000..09efa08 --- /dev/null +++ b/src/api/user/dMatchHistory.js @@ -0,0 +1,20 @@ +export async function dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStats) { + try { + if (!request.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + const userId = request.params.userId; + 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" }); + } + deleteMatchHistory.run(userId); + deleteStats.run(userId); + return reply.code(200).send({ msg: "Match history deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/dMember.js b/src/api/user/dMember.js new file mode 100644 index 0000000..240a297 --- /dev/null +++ b/src/api/user/dMember.js @@ -0,0 +1,25 @@ +export async function dMember(request, reply, fastify, getUserInfo, changeDisplayName) { + try { + if (!request.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + 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: "Display name deleted successfully" }); + } + 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" }); + } +} diff --git a/src/api/user/dUser.js b/src/api/user/dUser.js new file mode 100644 index 0000000..0b0ca0a --- /dev/null +++ b/src/api/user/dUser.js @@ -0,0 +1,14 @@ +export async function dUser(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteFriends, deleteUser) { + try { + if (!getUserInfo.get(request.params.userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + deleteMatchHistory.run(request.params.userId); + deleteFriends.run(request.params.userId); + deleteUser.run(request.params.userId); + return reply.code(200).send({ msg: "User deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/default.js b/src/api/user/default.js index ef9cf43..d122bf9 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -2,6 +2,23 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; import Database from 'better-sqlite3'; +import { gUsers } from './gUsers.js' +import { gUser } from './gUser.js' +import { gNumberUsers } from './gNumberUsers.js' +import { gFriends } from './gFriends.js' +import { gNumberFriends } from './gNumberFriends.js' +import { gMatchHistory } from './gMatchHistory.js' +import { gNumberMatches } from './gNumberMatches.js' +import { pUser } from './pUser.js' +import { pFriend } from './pFriend.js' +import { pMatchHistory } from './pMatchHistory.js' +import { uMember } from './uMember.js' +import { dUser } from './dUser.js' +import { dMember } from './dMember.js' +import { dFriends } from './dFriends.js' +import { dFriend } from './dFriend.js' +import { dMatchHistory } from './dMatchHistory.js' + const env = process.env.NODE_ENV || 'development'; let database; @@ -72,10 +89,9 @@ const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;' const deleteMatchHistory = database.prepare('DELETE FROM matchHistory WHERE username = ?;'); const deleteStats = database.prepare('UPDATE userData SET wins = 0, losses = 0 WHERE username = ?;'); -/** - * @param {import('fastify').FastifyInstance} fastify - * @param {import('fastify').FastifyPluginOptions} options - */ +const querySchema = { type: 'object', required: ['iStart', 'iEnd'], properties: { iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } } +const bodySchema = { type: 'object', required: ['opponent', 'myScore', 'opponentScore'], properties: { opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } } + export default async function(fastify, options) { fastify.register(fastifyJWT, { secret: process.env.JWT_SECRET || '123456789101112131415161718192021', @@ -106,347 +122,58 @@ export default async function(fastify, options) { }); // GET - fastify.get('/users', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const { iStart, iEnd } = request.query; - if (!iStart || !iEnd) { - return reply.code(400).send({ error: "Please specify both a starting and an ending index" }); - } - 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" }); - } - 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) => { - 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" }); - } + return gNumberUsers(request, reply, fastify, getNumberUsers); }); fastify.get('/users/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - const userInfo = getUserInfo.get(request.params.userId); - return reply.code(200).send({ userInfo }); - } 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" }); - } - const { iStart, iEnd } = request.query; - if (!iStart || !iEnd) { - return reply.code(400).send({ error: "Please specify both a starting and an ending index" }); - } - if (Number(iEnd) < Number(iStart)) { - return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); - } - const friends = getFriends.all(userId, Number(iEnd) - Number(iStart), Number(iStart)); - if (!friends.length) { - return reply.code(404).send({ error: "No friends exist in the selected range" }); - } - 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) => { - try { - const userId = request.params.userId; - 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" }); - } + return gNumberFriends(request, reply, fastify, getUserInfo, getNumberFriends); }); - fastify.get('/users/:userId/matchHistory', { 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" }); - } - const { iStart, iEnd } = request.query; - if (!iStart || !iEnd) { - return reply.code(400).send({ error: "Please specify both a starting and an ending index" }); - } - if (Number(iEnd) < Number(iStart)) { - return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); - } - const matchHistoryId = getMatchHistory.all(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 (id) => { - const res = await fetch('https://transcendence-api-scoreStore:3000/' + id, { method: "GET", headers: { "Content-Type": "application/json" } }); - if (!res.ok) - throw new Error('Failed to fetch item from blockchain API'); - return res.json(); - }); - 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" }); - } + fastify.get('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { querystring: querySchema } }, async (request, reply) => { + return gMatchHistory(request, reply, fastify, getUserInfo, getMatchHistory); }); fastify.get('/users/:userId/matchHistory/count', { 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" }); - } - const row = getNumberMatches.get(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" }); - } + return gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches); }); // POST fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => { - try { - const userId = request.params.userId; - if (!request.user || !request.user.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - if (request.user.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" }); - } + return pUser(request, reply, fastify, getUserInfo, createUser); }); fastify.post('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const userId = request.params.userId; - if (!request.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - 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 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" }); - } + return pFriend(request, reply, fastify, getUserInfo, getFriend, addFriend); }); - fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const userId = request.params.userId; - if (!request.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - if (request.user !== 'admin' && request.user !== userId) { - return reply.code(401).send({ error: "Unauthorized" }); - } - if (!request.body || !request.body.opponent || !request.body.p1Score || !request.body.p2Score) { - return reply.code(400).send({ error: "Please specify the opponent and the score of both players" }); - } - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - 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" }); - } - if (request.body.p1Score < 0 || request.body.p2Score < 0) { - return reply.code(400).send({ error: "A score cannot be strictly negative" }); - } - const res = await fetch('http://localhost:3003/', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ p1: userId, p2: request.body.opponent, p1Score: request.body.p1Score, p2Score: request.body.p2Score }) }); - if (!res.ok) - return reply.code(500).send({ error: "Internal server error" }); - addMatch.run(userId, res.id); - if (request.body.p1Score > request.body.p2Score) { - incWins.run(userId); - } else if (request.body.p1Score < request.body.p2Score) { - incLosses.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" }); - } + fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { body: bodySchema } }, async (request, reply) => { + return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWins, incLosses); }); // PATCH fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - const userId = request.params.userId; - if (!request.user || !request.user.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - if (request.user.user !== 'admin' && request.user.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: "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" }); - } + return uMember(request, reply, fastify, getUserInfo, changeDisplayName); }); // DELETE 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" }); - } - deleteMatchHistory.run(request.params.userId); - deleteFriends.run(request.params.userId); - deleteUser.run(request.params.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" }); - } + return dUser(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteFriends, deleteUser); }); fastify.delete('/users/:userId/:member', { preHandler: fastify.authenticate }, async (request, reply) => { - try { - if (!request.user || !request.user.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - const userId = request.params.userId; - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - const user = request.user.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: "Display name deleted successfully" }); - } - 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) => { - try { - if (!request.user || !request.user.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - const userId = request.params.userId; - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - if (request.user.user != 'admin' && request.user.user != userId) { - return reply.code(401).send({ error: "Unauthorized" }); - } - 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" }); - } + return dFriends(request, reply, fastify, getUserInfo, deleteFriends); }); fastify.delete('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - if (!request.user || !request.user.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - const userId = request.params.userId; - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - if (request.user.user != 'admin' && request.user.user != userId) { - return reply.code(401).send({ error: "Unauthorized" }); - } - const friendId = request.params.friendId; - if (!getFriend.get(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" }); - } + return dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend); }); fastify.delete('/users/:userId/matchHistory', { preHandler: [fastify.authenticate] }, async (request, reply) => { - try { - if (!request.user || !request.user.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - const userId = request.params.userId; - if (!getUserInfo.get(userId)) { - return reply.code(404).send({ error: "User does not exist" }); - } - if (request.user.user != 'admin' && request.user.user != userId) { - return reply.code(401).send({ error: "Unauthorized" }); - } - deleteMatchHistory.run(userId); - deleteStats.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" }); - } + return dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStats); }); } diff --git a/src/api/user/gFriends.js b/src/api/user/gFriends.js new file mode 100644 index 0000000..cc24986 --- /dev/null +++ b/src/api/user/gFriends.js @@ -0,0 +1,20 @@ +export async function gFriends(request, reply, fastify, getUserInfo, getFriends) { + try { + const userId = request.params.userId; + 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 friends = getFriends.all(userId, Number(iEnd) - Number(iStart), Number(iStart)); + if (!friends.length) { + return reply.code(404).send({ error: "No friends exist in the selected range" }); + } + return reply.code(200).send({ friends }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gMatchHistory.js b/src/api/user/gMatchHistory.js new file mode 100644 index 0000000..2566fe3 --- /dev/null +++ b/src/api/user/gMatchHistory.js @@ -0,0 +1,29 @@ +export async function gMatchHistory(request, reply, fastify, getUserInfo, getMatchHistory) { + try { + const userId = request.params.userId; + 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 matchHistoryId = getMatchHistory.all(userId, Number(iEnd) - Number(iStart), Number(iStart)); + if (!matchHistoryId.length) { + return reply.code(404).send({ error: "No matches exist in the selected range" }); + } + const ids = matchHistoryId.map(obj => Object.values(obj)[0]); + const promises = ids.map(async (id) => { + const res = await fetch(`http://localhost:3003/${id}`, { method: "GET" }); + if (!res.ok) { + throw new Error('Failed to fetch item from blockchain API'); + } + return await res.json(); + }); + const matchHistory = await Promise.all(promises); + return reply.code(200).send({ matchHistory }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gNumberFriends.js b/src/api/user/gNumberFriends.js new file mode 100644 index 0000000..ca1ec24 --- /dev/null +++ b/src/api/user/gNumberFriends.js @@ -0,0 +1,13 @@ +export async function gNumberFriends(request, reply, fastify, getUserInfo, getNumberFriends) { + try { + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const row = getNumberFriends.get(userId); + return reply.code(200).send({ n_friends: row.n_friends }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gNumberMatches.js b/src/api/user/gNumberMatches.js new file mode 100644 index 0000000..7abe3a6 --- /dev/null +++ b/src/api/user/gNumberMatches.js @@ -0,0 +1,13 @@ +export async function gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches) { + try { + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const row = getNumberMatches.get(userId); + return reply.code(200).send({ n_matches: row.n_matches }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gNumberUsers.js b/src/api/user/gNumberUsers.js new file mode 100644 index 0000000..b8c025d --- /dev/null +++ b/src/api/user/gNumberUsers.js @@ -0,0 +1,9 @@ +export async function gNumberUsers(request, reply, fastify, getNumberUsers) { + try { + const row = getNumberUsers.get(); + return reply.code(200).send({ n_users: row.n_users }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gUser.js b/src/api/user/gUser.js new file mode 100644 index 0000000..16a8439 --- /dev/null +++ b/src/api/user/gUser.js @@ -0,0 +1,13 @@ +export async function gUser(request, reply, fastify, getUserInfo) { + try { + const userId = request.params.userId; + const userInfo = getUserInfo.get(userId); + if (!userInfo) { + return reply.code(404).send({ error: "User does not exist" }); + } + return reply.code(200).send({ userInfo }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/gUsers.js b/src/api/user/gUsers.js new file mode 100644 index 0000000..4a60d6d --- /dev/null +++ b/src/api/user/gUsers.js @@ -0,0 +1,16 @@ +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" }); + } + return reply.code(200).send({ users }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pFriend.js b/src/api/user/pFriend.js new file mode 100644 index 0000000..dfb5341 --- /dev/null +++ b/src/api/user/pFriend.js @@ -0,0 +1,29 @@ +export async function pFriend(request, reply, fastify, getUserInfo, getFriend, addFriend) { + try { + const userId = request.params.userId; + if (!request.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + 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 friendId = request.params.friendId; + if (!getUserInfo.get(friendId)) { + return reply.code(404).send({ error: "Friend does not exist" }); + } + if (friendId === userId) { + return reply.code(400).send({ error: "You can't add yourself :D" }); + } + if (getFriend.get(userId, friendId)) { + return reply.code(400).send({ error: "Friend already added" }); + } + addFriend.run(userId, friendId) + return reply.code(200).send({ msg: "Friend added successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pMatchHistory.js b/src/api/user/pMatchHistory.js new file mode 100644 index 0000000..d619149 --- /dev/null +++ b/src/api/user/pMatchHistory.js @@ -0,0 +1,36 @@ +export async function pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWins, incLosses) { + try { + const userId = request.params.userId; + if (!request.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + 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" }); + } + 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" }); + } + const res = await fetch('http://localhost:3003/', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ p1: userId, p2: request.body.opponent, p1Score: request.body.myScore, p2Score: request.body.opponentScore }) }); + if (!res.ok) + return reply.code(500).send({ error: "Internal server error" }); + const data = await res.json(); + addMatch.run(userId, data.id); + if (request.body.myScore > request.body.opponentScore) { + incWins.run(userId); + incLosses.run(request.body.opponent); + } else if (request.body.myScore < request.body.opponentScore) { + incWins.run(request.body.opponent); + incLosses.run(userId); + } + return reply.code(200).send({ msg: "Match successfully saved to the blockchain" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pUser.js b/src/api/user/pUser.js new file mode 100644 index 0000000..c5dd88e --- /dev/null +++ b/src/api/user/pUser.js @@ -0,0 +1,22 @@ +export async function pUser(request, reply, fastify, getUserInfo, createUser) { + try { + const userId = request.params.userId; + if (!request.user || !request.user.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + if (request.user.user !== 'admin') { + return reply.code(401).send({ error: "Unauthorized" }); + } + if (getUserInfo.get(userId)) { + return reply.code(400).send({ error: "User already exist" }); + } + if (!request.body || !request.body.displayName) { + return reply.code(400).send({ error: "Please specify a display name" }); + } + createUser.run(userId, request.body.displayName); + return reply.code(200).send({ msg: "User created successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/uMember.js b/src/api/user/uMember.js new file mode 100644 index 0000000..67997d7 --- /dev/null +++ b/src/api/user/uMember.js @@ -0,0 +1,26 @@ +export async function uMember(request, reply, fastify, getUserInfo, changeDisplayName) { + try { + const userId = request.params.userId; + if (!request.user) { + return reply.code(400).send({ error: "Please specify a user" }); + } + 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: "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" }); + } +} From 7333dc2602070379897db20564691d3e3b8b1187 Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Thu, 2 Oct 2025 16:06:51 +0200 Subject: [PATCH 05/45] forgot to push the doc --- doc/user/user.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/doc/user/user.md b/doc/user/user.md index 3c18841..7821997 100644 --- a/doc/user/user.md +++ b/doc/user/user.md @@ -95,8 +95,8 @@ Input needed : ```json { "opponent": "", - "p1Score": , - "p2Score": + "myScore": , + "opponentScore": } ``` @@ -247,19 +247,22 @@ Used to get the match history of an user Can return: - 200 with response (list of matches results (between iStart and iEnd)) ```json -[ - { - "score": +{ + "matchHistory": + [ { - "p1": "", - "p2": "", - "p1Score": "", - "p2Score": "" + "score": + { + "p1": "", + "p2": "", + "p1Score": "", + "p2Score": "" + }, + "tx": "" }, - "tx": "" - }, - ... -] + ... + ] +} ``` - 400 with response (if iStart/iEnd does not exist, or iEnd < iStart) ```json From dfb2541267e492e32dd5727f4162e68ff122d19a Mon Sep 17 00:00:00 2001 From: adjoly Date: Tue, 7 Oct 2025 20:29:49 +0200 Subject: [PATCH 06/45] =?UTF-8?q?=E3=80=8C=F0=9F=8F=97=EF=B8=8F=E3=80=8D?= =?UTF-8?q?=20wip(scoreStore):=20simingly=20working=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/scoreStore/addTx.js | 9 ++++----- src/utils/scoreStore_contract.js | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/api/scoreStore/addTx.js b/src/api/scoreStore/addTx.js index 413086b..e9682fa 100644 --- a/src/api/scoreStore/addTx.js +++ b/src/api/scoreStore/addTx.js @@ -9,12 +9,11 @@ import { callAddScore, callLastId } from "../../utils/scoreStore_contract.js"; */ export async function addTx(request, reply, fastify) { try { - const id = await callLastId(); - const tx = callAddScore(request.body.p1, request.body.p2, request.body.p1Score, request.body.p2Score); + const {tx, id} = await callAddScore(request.body.p1, request.body.p2, request.body.p1Score, request.body.p2Score); - tx.then(tx => { - scoreDB.addTx(id, tx.hash); - }); + scoreDB.addTx(id, tx.hash); + // tx.then(tx => { + // }); return reply.code(200).send({ id: Number(id) diff --git a/src/utils/scoreStore_contract.js b/src/utils/scoreStore_contract.js index b8ada16..f3e6ef2 100644 --- a/src/utils/scoreStore_contract.js +++ b/src/utils/scoreStore_contract.js @@ -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; From fd801d267d13a1b34639d7343c05ef96f899a3da Mon Sep 17 00:00:00 2001 From: adjoly Date: Tue, 7 Oct 2025 20:39:07 +0200 Subject: [PATCH 07/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20s?= =?UTF-8?q?hould=20be=20working=20as=20expected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/scoreStore_contract.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/scoreStore_contract.js b/src/utils/scoreStore_contract.js index f3e6ef2..d9f626b 100644 --- a/src/utils/scoreStore_contract.js +++ b/src/utils/scoreStore_contract.js @@ -29,7 +29,7 @@ async function loadContract() { async function callGetScore(id) { try { const contract = await loadContract(); - const result = await contract.getScore(id); + const result = await contract.getScore(id - 1); return result; } catch (error) { console.error('Error calling view function:', error); From 005e9ebbbb9eb4bb88710df905c47b51ab0c6a1f Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Wed, 15 Oct 2025 14:02:59 +0200 Subject: [PATCH 08/45] fixed an inconsistency with the documentation --- src/api/user/gUser.js | 2 +- src/api/user/pMatchHistory.js | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/api/user/gUser.js b/src/api/user/gUser.js index 16a8439..4da2426 100644 --- a/src/api/user/gUser.js +++ b/src/api/user/gUser.js @@ -5,7 +5,7 @@ export async function gUser(request, reply, fastify, getUserInfo) { if (!userInfo) { return reply.code(404).send({ error: "User does not exist" }); } - return reply.code(200).send({ userInfo }); + return reply.code(200).send({ username: userInfo.username, displayName: userInfo.displayName, wins: userInfo.wins, losses: userInfo.losses }); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); diff --git a/src/api/user/pMatchHistory.js b/src/api/user/pMatchHistory.js index d619149..1cf17b4 100644 --- a/src/api/user/pMatchHistory.js +++ b/src/api/user/pMatchHistory.js @@ -1,3 +1,12 @@ +async function fetchSave(request, reply, userId, addMatch) { + const res = await fetch('http://localhost:3003/', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ p1: userId, p2: request.body.opponent, p1Score: request.body.myScore, p2Score: request.body.opponentScore }) }); + if (!res.ok) { + throw new Error('Internal server error'); + } + const data = await res.json(); + addMatch.run(userId, data.id); +} + export async function pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWins, incLosses) { try { const userId = request.params.userId; @@ -16,11 +25,7 @@ export async function pMatchHistory(request, reply, fastify, getUserInfo, addMat if (request.body.opponent === userId) { return reply.code(400).send({ error: "Do you have dementia ? You cannot have played a match against yourself gramps" }); } - const res = await fetch('http://localhost:3003/', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ p1: userId, p2: request.body.opponent, p1Score: request.body.myScore, p2Score: request.body.opponentScore }) }); - if (!res.ok) - return reply.code(500).send({ error: "Internal server error" }); - const data = await res.json(); - addMatch.run(userId, data.id); + await fetchSave(request, reply, userId, addMatch); if (request.body.myScore > request.body.opponentScore) { incWins.run(userId); incLosses.run(request.body.opponent); From e4e53e06f67b6cd518a235769c0d039f2568a310 Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Wed, 15 Oct 2025 18:19:51 +0200 Subject: [PATCH 09/45] added tetris game management --- doc/user/user.md | 49 +++++++++++++++++++--------- src/api/user/dMatchHistory.js | 15 +++++++-- src/api/user/dUser.js | 3 +- src/api/user/default.js | 58 +++++++++++++++++++++------------- src/api/user/gMatchHistory.js | 7 ++-- src/api/user/gNumberMatches.js | 6 +++- src/api/user/gUser.js | 2 +- src/api/user/gUsers.js | 14 +++++++- src/api/user/pMatchHistory.js | 30 +++++++++++++----- 9 files changed, 130 insertions(+), 54 deletions(-) diff --git a/doc/user/user.md b/doc/user/user.md index 7821997..da1a991 100644 --- a/doc/user/user.md +++ b/doc/user/user.md @@ -87,13 +87,14 @@ Can return: } ``` -## POST `/users/:userId/matchHistory` +## POST `/users/:userId/matchHistory?game=` -Used to add a match result to an user +Used to add a match result to an user to a specific game Input needed : ```json { + "game": "" "opponent": "", "myScore": , "opponentScore": @@ -107,7 +108,7 @@ Can return: "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) +- 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) ```json { "error": "" @@ -139,8 +140,14 @@ Can return: { "username": "", "displayName": "", - "wins": , - "losses": + "pong": { + "wins": , + "losses": + }, + "tetris": { + "wins": , + "losses": + } }, ... ] @@ -181,8 +188,14 @@ Can return: { "username": "", "displayName": "", - "wins": , - "losses": + "pong": { + "wins": , + "losses": + }, + "tetris": { + "wins": , + "losses": + } } ``` - 404 with response (if user does not exist) @@ -240,9 +253,9 @@ Can return: } ``` -## GET `/users/:userId/matchHistory?iStart=&iEnd=` +## GET `/users/:userId/matchHistory?game=&iStart=&iEnd=` -Used to get the match history of an user +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)) @@ -264,7 +277,7 @@ Can return: ] } ``` -- 400 with response (if iStart/iEnd does not exist, or iEnd < iStart) +- 400 with response (if iStart/iEnd does not exist, or iEnd < iStart, or the game specified is invalid) ```json { "error": "" @@ -277,9 +290,9 @@ Can return: } ``` -## GET `/users/:userId/matchHistory/count` +## GET `/users/:userId/matchHistory/count?game=` -Used to get the number of matches an user played +Used to get the number of matches an user played for a specific game Can return: - 200 with response @@ -288,6 +301,12 @@ Can return: "n_": } ``` +- 400 with response (if game does not exist) +```json +{ + "error": "" +} +``` - 404 with response (if user does not exist) ```json { @@ -440,9 +459,9 @@ Can return: } ``` -## DELETE `/users/:userId/matchHistory` +## DELETE `/users/:userId/matchHistory?game=` -Used to delete the match history of an user +Used to delete the match history of an user for a specific game Can return: - 200 with response @@ -451,7 +470,7 @@ Can return: "msg": "Match history deleted successfully" } ``` -- 400 with response (if user specified in header is neither admin nor user) +- 400 with response (if user specified in header is neither admin nor user, or the game specified is invalid) ```json { "error": "" diff --git a/src/api/user/dMatchHistory.js b/src/api/user/dMatchHistory.js index 09efa08..8b7cc0e 100644 --- a/src/api/user/dMatchHistory.js +++ b/src/api/user/dMatchHistory.js @@ -1,4 +1,4 @@ -export async function dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStats) { +export async function dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris) { try { if (!request.user) { return reply.code(400).send({ error: "Please specify a user" }); @@ -10,8 +10,17 @@ export async function dMatchHistory(request, reply, fastify, getUserInfo, delete if (request.user !== 'admin' && request.user !== userId) { return reply.code(401).send({ error: "Unauthorized" }); } - deleteMatchHistory.run(userId); - deleteStats.run(userId); + 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); diff --git a/src/api/user/dUser.js b/src/api/user/dUser.js index 0b0ca0a..d2728af 100644 --- a/src/api/user/dUser.js +++ b/src/api/user/dUser.js @@ -3,7 +3,8 @@ export async function dUser(request, reply, fastify, getUserInfo, deleteMatchHis if (!getUserInfo.get(request.params.userId)) { return reply.code(404).send({ error: "User does not exist" }); } - deleteMatchHistory.run(request.params.userId); + deleteMatchHistory.run('pong', request.params.userId); + deleteMatchHistory.run('tetris', request.params.userId); deleteFriends.run(request.params.userId); deleteUser.run(request.params.userId); return reply.code(200).send({ msg: "User deleted successfully" }); diff --git a/src/api/user/default.js b/src/api/user/default.js index d122bf9..22ca384 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -35,11 +35,15 @@ function prepareDB() { id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, displayName TEXT, - wins INTEGER, - losses INTEGER, + pongWins INTEGER, + pongLosses INTEGER, + tetrisWins INTEGER, + tetrisLosses INTEGER, UNIQUE(username), - CHECK(wins >= 0), - CHECK(losses >= 0) + CHECK(pongWins >= 0), + CHECK(pongLosses >= 0), + CHECK(tetrisWins >= 0), + CHECK(tetrisLosses >= 0) ) STRICT `); database.exec(` @@ -54,8 +58,12 @@ function prepareDB() { database.exec(` CREATE TABLE IF NOT EXISTS matchHistory ( id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT, - matchId INTEGER + game TEXT, + player1 TEXT, + player2 TEXT, + matchId INTEGER, + CHECK(player1 != player2), + CHECK(game = 'pong' OR game = 'tetris') ) STRICT `); } @@ -63,34 +71,40 @@ function prepareDB() { prepareDB(); // POST -const createUser = database.prepare('INSERT INTO userData (username, displayName, wins, losses) VALUES (?, ?, 0, 0);'); +const createUser = database.prepare('INSERT INTO userData (username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses) VALUES (?, ?, 0, 0, 0, 0);'); const addFriend = database.prepare('INSERT INTO friends (username, friendName) VALUES (?, ?);'); -const addMatch = database.prepare('INSERT INTO matchHistory (username, matchId) VALUES (?, ?);'); -const incWins = database.prepare('UPDATE userData SET wins = wins + 1 WHERE username = ?;'); -const incLosses = database.prepare('UPDATE userData SET losses = losses + 1 WHERE username = ?'); +const addMatch = database.prepare('INSERT INTO matchHistory (game, 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 = ?'); // PATCH const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;'); // GET -const getUserData = database.prepare('SELECT username, displayName, wins, losses FROM userData LIMIT ? OFFSET ?;'); -const getUserInfo = database.prepare('SELECT username, displayName, wins, losses FROM userData WHERE username = ?;'); +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 FROM matchHistory WHERE username = ? LIMIT ? OFFSET ?;'); +const getMatchHistory = database.prepare('SELECT matchId 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 username = ?;') +const getNumberMatches = database.prepare('SELECT COUNT (DISTINCT id) AS n_matches FROM matchHistory WHERE game = ? AND ? IN (player1, player2);') // 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 username = ?;'); -const deleteStats = database.prepare('UPDATE userData SET wins = 0, losses = 0 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 querySchema = { type: 'object', required: ['iStart', 'iEnd'], properties: { iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } } const bodySchema = { type: 'object', required: ['opponent', 'myScore', 'opponentScore'], properties: { opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } } +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', 'opponent', 'myScore', 'opponentScore'], properties: { game: { type: 'string' }, opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } } +const querySchemaMatchHistoryGame = { type: 'object', required: ['game'], properties: { game: { type: 'string' } } } export default async function(fastify, options) { fastify.register(fastifyJWT, { @@ -137,10 +151,10 @@ export default async function(fastify, options) { 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: querySchema } }, async (request, reply) => { + 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] }, async (request, reply) => { + fastify.get('/users/:userId/matchHistory/count', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => { return gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches); }); @@ -151,8 +165,8 @@ export default async function(fastify, options) { 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: bodySchema } }, async (request, reply) => { - return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWins, incLosses); + 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); }); // PATCH @@ -173,7 +187,7 @@ export default async function(fastify, options) { fastify.delete('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => { return dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend); }); - fastify.delete('/users/:userId/matchHistory', { preHandler: [fastify.authenticate] }, async (request, reply) => { - return dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStats); + fastify.delete('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => { + return dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris); }); } diff --git a/src/api/user/gMatchHistory.js b/src/api/user/gMatchHistory.js index 2566fe3..6fde63e 100644 --- a/src/api/user/gMatchHistory.js +++ b/src/api/user/gMatchHistory.js @@ -4,11 +4,14 @@ export async function gMatchHistory(request, reply, fastify, getUserInfo, getMat if (!getUserInfo.get(userId)) { return reply.code(404).send({ error: "User does not exist" }); } - const { iStart, iEnd } = request.query; + 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(userId, Number(iEnd) - Number(iStart), Number(iStart)); + 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" }); } diff --git a/src/api/user/gNumberMatches.js b/src/api/user/gNumberMatches.js index 7abe3a6..f26e628 100644 --- a/src/api/user/gNumberMatches.js +++ b/src/api/user/gNumberMatches.js @@ -4,7 +4,11 @@ export async function gNumberMatches(request, reply, fastify, getUserInfo, getNu if (!getUserInfo.get(userId)) { return reply.code(404).send({ error: "User does not exist" }); } - const row = getNumberMatches.get(userId); + 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); diff --git a/src/api/user/gUser.js b/src/api/user/gUser.js index 4da2426..6937e6c 100644 --- a/src/api/user/gUser.js +++ b/src/api/user/gUser.js @@ -5,7 +5,7 @@ export async function gUser(request, reply, fastify, getUserInfo) { if (!userInfo) { return reply.code(404).send({ error: "User does not exist" }); } - return reply.code(200).send({ username: userInfo.username, displayName: userInfo.displayName, wins: userInfo.wins, losses: userInfo.losses }); + return reply.code(200).send({ username: userInfo.username, displayName: userInfo.displayName, pong: { wins: userInfo.pongWins, losses: userInfo.pongLosses }, tetris: { wins: userInfo.tetrisWins, losses: userInfo.tetrisLosses } }); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); diff --git a/src/api/user/gUsers.js b/src/api/user/gUsers.js index 4a60d6d..bf42f33 100644 --- a/src/api/user/gUsers.js +++ b/src/api/user/gUsers.js @@ -8,7 +8,19 @@ export async function gUsers(request, reply, fastify, getUserData) { if (!users.length) { return reply.code(404).send({ error: "No users exist in the selected range" }); } - return reply.code(200).send({ users }); + const usersFormat = users.map(obj => ({ + username: obj.username, + displayName: obj.displayName, + pong: { + wins: obj.pongWins, + losses: obj.pongLosses + }, + tetris: { + wins: obj.tetrisWins, + losses: obj.tetrisLosses + } + })); + return reply.code(200).send({ usersFormat }); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); diff --git a/src/api/user/pMatchHistory.js b/src/api/user/pMatchHistory.js index 1cf17b4..fc346d1 100644 --- a/src/api/user/pMatchHistory.js +++ b/src/api/user/pMatchHistory.js @@ -4,10 +4,10 @@ async function fetchSave(request, reply, userId, addMatch) { throw new Error('Internal server error'); } const data = await res.json(); - addMatch.run(userId, data.id); + addMatch.run(request.body.game, userId, request.body.opponent, data.id); } -export async function pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWins, incLosses) { +export async function pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris) { try { const userId = request.params.userId; if (!request.user) { @@ -16,6 +16,9 @@ export async function pMatchHistory(request, reply, fastify, getUserInfo, addMat if (request.user !== 'admin' && request.user !== userId) { return reply.code(401).send({ error: "Unauthorized" }); } + if (request.body.game !== 'pong' && request.body.game !== 'tetris') { + return reply.code(400).send({ error: "Specified game does not exist" }); + } if (!getUserInfo.get(userId)) { return reply.code(404).send({ error: "User does not exist" }); } @@ -26,12 +29,23 @@ export async function pMatchHistory(request, reply, fastify, getUserInfo, addMat return reply.code(400).send({ error: "Do you have dementia ? You cannot have played a match against yourself gramps" }); } await fetchSave(request, reply, userId, addMatch); - if (request.body.myScore > request.body.opponentScore) { - incWins.run(userId); - incLosses.run(request.body.opponent); - } else if (request.body.myScore < request.body.opponentScore) { - incWins.run(request.body.opponent); - incLosses.run(userId); + 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') { + 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) { From f689274716965af466496207f6b91191a0c073cf Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 16 Oct 2025 12:29:16 +0200 Subject: [PATCH 10/45] =?UTF-8?q?=E3=80=8C=F0=9F=8F=97=EF=B8=8F=E3=80=8D?= =?UTF-8?q?=20wip(auth):=20added=20the=20hability=20to=20remove=20user?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/auth/remove.md | 32 ++++++++++++++++++++++++++++++++ src/api/auth/default.js | 3 +++ src/api/auth/remove.js | 34 ++++++++++++++++++++++++++++++++++ src/utils/authUserRemove.js | 19 +++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 doc/auth/remove.md create mode 100644 src/api/auth/remove.js create mode 100644 src/utils/authUserRemove.js diff --git a/doc/auth/remove.md b/doc/auth/remove.md new file mode 100644 index 0000000..366c89c --- /dev/null +++ b/doc/auth/remove.md @@ -0,0 +1,32 @@ +# remove user + +Available endpoints: +- DELETE `/` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## DELETE `/` + +User to remove a user from the backend + +Inputs: just need a valid JWT cookie + +Returns: +- 200 +```json +{ + "msg": "User successfully deleted" +} +``` +- 401 || 400 +```json +{ + "error": " +} +``` diff --git a/src/api/auth/default.js b/src/api/auth/default.js index 0e491ac..422d794 100644 --- a/src/api/auth/default.js +++ b/src/api/auth/default.js @@ -11,6 +11,7 @@ import { totpSetup } from './totpSetup.js'; import { totpDelete } from './totpDelete.js'; import { totpVerify } from './totpVerify.js'; import { logout } from './logout.js'; +import { remove } from './remove.js'; const saltRounds = 10; export const appName = process.env.APP_NAME || 'knl_meowscendence'; @@ -110,4 +111,6 @@ export default async function(fastify, options) { }, async (request, reply) => { return register(request, reply, saltRounds, fastify); }); fastify.get('/logout', {}, async (request, reply) => { return logout(reply, fastify); }) + + fastify.delete('/', { preHandler: fastify.authenticate }, async (request, reply) => { return remove(request, reply, fastify)}) } diff --git a/src/api/auth/remove.js b/src/api/auth/remove.js new file mode 100644 index 0000000..90579af --- /dev/null +++ b/src/api/auth/remove.js @@ -0,0 +1,34 @@ +import authDB from '../../utils/authDB'; +import { authUserRemove } from '../../utils/authUserRemove'; + +/** + * @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) + .send({ + msg: "User successfully deleted" + }) + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/utils/authUserRemove.js b/src/utils/authUserRemove.js new file mode 100644 index 0000000..071a51f --- /dev/null +++ b/src/utils/authUserRemove.js @@ -0,0 +1,19 @@ +import axios from 'axios' + +/** + * @param {string} username + * @param {import('fastify').FastifyInstance} fastify + */ +export async function authUserRemove(username, fastify) { + const url = (process.env.USER_URL || "http://localhost:3002/") + "users/" + username; + const cookie = fastify.jwt.sign({ user: "admin" }); + + await axios.post( + url, + { + headers: { + 'Cookie': 'token=' + cookie, + }, + } + ); +} From 95cbbc6613273ec17402755298cb60c1b004e34d Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 16 Oct 2025 12:33:49 +0200 Subject: [PATCH 11/45] =?UTF-8?q?=E3=80=8C=F0=9F=93=9D=E3=80=8D=20doc(auth?= =?UTF-8?q?):=20added=20doc=20for=202fa=20in=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/auth/login.md | 11 +++++++++-- src/api/auth/login.js | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/doc/auth/login.md b/doc/auth/login.md index 76945d3..eff517d 100644 --- a/doc/auth/login.md +++ b/doc/auth/login.md @@ -21,7 +21,8 @@ Input needed : ```json { "user": "", - "password": "" + "password": "", + (optional)"token": "<2fa token>" } ``` @@ -32,7 +33,13 @@ Can return: "msg": "Login successfully" } ``` -- 400 with response +- 402 with response +```json +{ + "msg": "Please specify a 2fa token" +} +``` +- 400 || 401 with response ```json { "error": "" diff --git a/src/api/auth/login.js b/src/api/auth/login.js index 2580383..83d9a76 100644 --- a/src/api/auth/login.js +++ b/src/api/auth/login.js @@ -37,8 +37,8 @@ export async function login(request, reply, fastify) { const userTOTP = authDB.getUser(user); if (userTOTP.totpEnabled == 1) { - if (!request.body.token){ - return reply.code(401).send({ error: 'Invalid 2FA token' }); + if (!request.body.token) { + return reply.code(402).send({ error: 'Please specify a 2fa token' }); } const isValid = verifyTOTP(userTOTP.totpHash, request.body.token); if (!isValid) { From d1838e7722673b62ee9451288bbdec3249f9f7bd Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 16 Oct 2025 13:27:19 +0200 Subject: [PATCH 12/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20f?= =?UTF-8?q?ixed=20some=20things.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/authUserRemove.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/authUserRemove.js b/src/utils/authUserRemove.js index 071a51f..ba892fc 100644 --- a/src/utils/authUserRemove.js +++ b/src/utils/authUserRemove.js @@ -5,7 +5,7 @@ import axios from 'axios' * @param {import('fastify').FastifyInstance} fastify */ export async function authUserRemove(username, fastify) { - const url = (process.env.USER_URL || "http://localhost:3002/") + "users/" + username; + const url = ((process.env.USER_URL + "/") || "http://localhost:3002/") + "users/" + username; const cookie = fastify.jwt.sign({ user: "admin" }); await axios.post( From 169386a97c776119b7085d62191ddb07a1f7d6a9 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 16 Oct 2025 13:28:43 +0200 Subject: [PATCH 13/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20f?= =?UTF-8?q?ixed=20this=20shit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/authUserRemove.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/authUserRemove.js b/src/utils/authUserRemove.js index ba892fc..8af7503 100644 --- a/src/utils/authUserRemove.js +++ b/src/utils/authUserRemove.js @@ -5,7 +5,7 @@ import axios from 'axios' * @param {import('fastify').FastifyInstance} fastify */ export async function authUserRemove(username, fastify) { - const url = ((process.env.USER_URL + "/") || "http://localhost:3002/") + "users/" + username; + const url = (process.env.USER_URL || "http://localhost:3002") + "/users/" + username; const cookie = fastify.jwt.sign({ user: "admin" }); await axios.post( From cd03f63d01553b13849e1a5e4f7d0e8c91a7cb65 Mon Sep 17 00:00:00 2001 From: Adam <45126464+KeyZox71@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:26:54 +0200 Subject: [PATCH 14/45] =?UTF-8?q?=E3=80=8C=F0=9F=93=9D=E3=80=8D=20doc(READ?= =?UTF-8?q?ME):=20updated=20TODO=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated module completion status and removed completed tasks. --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9d75505..b7c08ae 100644 --- a/README.md +++ b/README.md @@ -18,27 +18,27 @@ Press F to pay respect ``` ## Modules done -6 major + 2 minor = 7 full modules +8 major + 4 minor = 10 full modules - **Web** - [x] Use a framework to build the backend.(node with Fastify) - Major - - [ ] Use a framework or toolkit to build the front-end.(Tailwind CSS) - Minor + - [x] Use a framework or toolkit to build the front-end.(Tailwind CSS) - Minor - [x] Use a database for the backend -and more.(SQLite) - Minor - [x] Store the score of a tournament in the Blockchain.(Soldity on Avalanche) - Major - **User Management** - - [ ] Standard user management, authentication and users across tournaments. - Major + - [x] Standard user management, authentication and users across tournaments. - Major - [x] Implement remote authentication. - Major - **Gameplay and user experience** - - [ ] Remote players - Major - - [ ] Multiplayer - Major - - [ ] Add another game - Major - - [ ] Game customization options - Minor - - [ ] Live chat - Major + - [ ] ~~Remote players - Major~~ + - [ ] ~~Multiplayer - Major~~ + - [x] Add another game - Major + - [ ] ~~Game customization options - Minor~~ + - [ ] ~~Live chat - Major~~ - **AI-Algo** - - [ ] AI opponent - Major + - [ ] ~~AI opponent - Major~~ - [ ] User and game stats dashboards - Minor - **Cybersecurity** - - [ ] WAF/ModSecurity and Hashicorp Vault - Major + - [ ] ~~WAF/ModSecurity and Hashicorp Vault - Major~~ - [ ] RGPD compliance - Minor - [x] 2FA and JWT - Major - **DevOps** @@ -46,16 +46,16 @@ Press F to pay respect - [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 From 1f085ce1fbc1e6f58ad50beed8a0fc92d91710cd Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Fri, 17 Oct 2025 14:39:46 +0200 Subject: [PATCH 15/45] added date to match objects --- doc/user/user.md | 12 +++++++----- src/api/user/default.js | 10 ++++++---- src/api/user/gMatchHistory.js | 9 +++++---- src/api/user/pMatchHistory.js | 2 +- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/doc/user/user.md b/doc/user/user.md index da1a991..d982e68 100644 --- a/doc/user/user.md +++ b/doc/user/user.md @@ -97,7 +97,8 @@ Input needed : "game": "" "opponent": "", "myScore": , - "opponentScore": + "opponentScore": , + "date": } ``` @@ -174,7 +175,7 @@ Always returns: - 200 with response ```json { - "n_": + "n_users": } ``` @@ -243,7 +244,7 @@ Can return: - 200 with response ```json { - "n_": + "n_friends": } ``` - 404 with response (if user does not exist) @@ -269,7 +270,8 @@ Can return: "p1": "", "p2": "", "p1Score": "", - "p2Score": "" + "p2Score": "", + "date": }, "tx": "" }, @@ -298,7 +300,7 @@ Can return: - 200 with response ```json { - "n_": + "n_matches": } ``` - 400 with response (if game does not exist) diff --git a/src/api/user/default.js b/src/api/user/default.js index 22ca384..a2cc806 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -59,11 +59,13 @@ function prepareDB() { CREATE TABLE IF NOT EXISTS matchHistory ( id INTEGER PRIMARY KEY AUTOINCREMENT, game TEXT, + date INTEGER, player1 TEXT, player2 TEXT, matchId INTEGER, CHECK(player1 != player2), - CHECK(game = 'pong' OR game = 'tetris') + CHECK(game = 'pong' OR game = 'tetris'), + CHECK(date >= 0) ) STRICT `); } @@ -73,7 +75,7 @@ prepareDB(); // POST const createUser = database.prepare('INSERT INTO userData (username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses) VALUES (?, ?, 0, 0, 0, 0);'); const addFriend = database.prepare('INSERT INTO friends (username, friendName) VALUES (?, ?);'); -const addMatch = database.prepare('INSERT INTO matchHistory (game, player1, player2, matchId) 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 = ?;'); @@ -87,7 +89,7 @@ const getUserData = database.prepare('SELECT username, displayName, pongWins, po 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 FROM matchHistory WHERE game = ? AND ? IN (player1, player2) LIMIT ? OFFSET ?;'); +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);') @@ -103,7 +105,7 @@ const deleteStatsTetris = database.prepare('UPDATE userData SET tetrisWins = 0, const querySchema = { type: 'object', required: ['iStart', 'iEnd'], properties: { iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } } const bodySchema = { type: 'object', required: ['opponent', 'myScore', 'opponentScore'], properties: { opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } } 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', 'opponent', 'myScore', 'opponentScore'], properties: { game: { type: 'string' }, opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } } +const bodySchemaMatchHistory = { type: 'object', required: ['game', 'date', 'opponent', 'myScore', 'opponentScore'], 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' } } } export default async function(fastify, options) { diff --git a/src/api/user/gMatchHistory.js b/src/api/user/gMatchHistory.js index 6fde63e..7531ca0 100644 --- a/src/api/user/gMatchHistory.js +++ b/src/api/user/gMatchHistory.js @@ -15,13 +15,14 @@ export async function gMatchHistory(request, reply, fastify, getUserInfo, getMat if (!matchHistoryId.length) { return reply.code(404).send({ error: "No matches exist in the selected range" }); } - const ids = matchHistoryId.map(obj => Object.values(obj)[0]); - const promises = ids.map(async (id) => { - const res = await fetch(`http://localhost:3003/${id}`, { method: "GET" }); + const promises = matchHistoryId.map(async (match) => { + const res = await fetch(`http://localhost:3003/${match.matchId}`, { method: "GET" }); if (!res.ok) { throw new Error('Failed to fetch item from blockchain API'); } - return await res.json(); + const resJson = await res.json(); + resJson.score.date = match.date; + return resJson; }); const matchHistory = await Promise.all(promises); return reply.code(200).send({ matchHistory }); diff --git a/src/api/user/pMatchHistory.js b/src/api/user/pMatchHistory.js index fc346d1..f22ef38 100644 --- a/src/api/user/pMatchHistory.js +++ b/src/api/user/pMatchHistory.js @@ -4,7 +4,7 @@ async function fetchSave(request, reply, userId, addMatch) { throw new Error('Internal server error'); } const data = await res.json(); - addMatch.run(request.body.game, userId, request.body.opponent, data.id); + addMatch.run(request.body.game, request.body.date, userId, request.body.opponent, data.id); } export async function pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris) { From 0fdbc96dc45acc46edcc561ac74c73c7a84f0367 Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Fri, 17 Oct 2025 20:06:54 +0200 Subject: [PATCH 16/45] unfinished image API --- Justfile | 6 +- package.json | 2 + pnpm-lock.yaml | 288 ++++++++++++++++++++++++++++++++++++++ src/api/auth/remove.js | 4 +- src/api/images/dImage.js | 10 ++ src/api/images/default.js | 86 ++++++++++++ src/api/images/gImage.js | 13 ++ src/api/images/pImage.js | 33 +++++ src/api/user/TODO | 8 -- src/api/user/dAvatar.js | 9 ++ src/api/user/default.js | 55 +++++--- src/api/user/gAvatar.js | 9 ++ src/api/user/pAvatar.js | 13 ++ src/api/user/uMember.js | 2 +- src/start.js | 11 ++ 15 files changed, 518 insertions(+), 31 deletions(-) create mode 100644 src/api/images/dImage.js create mode 100644 src/api/images/default.js create mode 100644 src/api/images/gImage.js create mode 100644 src/api/images/pImage.js delete mode 100644 src/api/user/TODO create mode 100644 src/api/user/dAvatar.js create mode 100644 src/api/user/gAvatar.js create mode 100644 src/api/user/pAvatar.js diff --git a/Justfile b/Justfile index a9024ab..fe4ea0f 100644 --- a/Justfile +++ b/Justfile @@ -11,6 +11,10 @@ set dotenv-load @user $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true": fastify start src/api/user/default.js +# For launching the images api +@images $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true": + fastify start src/api/images/default.js + @scoreStore $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true": fastify start src/api/scoreStore/default.js @@ -55,4 +59,4 @@ set dotenv-load forge verify-contract --chain-id 43113 --rpc-url=${AVAX_RPC_URL} --watch ${AVAX_CONTRACT_ADDR} @status: - docker compose -f docker/docker-compose.yml ps \ No newline at end of file + docker compose -f docker/docker-compose.yml ps diff --git a/package.json b/package.json index 7eedbe9..32cd1ca 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "@fastify/cookie": "^11.0.2", "@fastify/env": "^5.0.2", "@fastify/jwt": "^9.1.0", + "@fastify/multipart": "^9.2.1", "axios": "^1.10.0", "base32.js": "^0.1.0", "bcrypt": "^6.0.0", @@ -13,6 +14,7 @@ "fastify-cli": "^7.4.0", "pino": "^9.7.0", "prom-client": "^15.1.3", + "sharp": "^0.34.4", "solhint": "^6.0.0" }, "type": "module", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91f44c7..c40ec77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@fastify/jwt': specifier: ^9.1.0 version: 9.1.0 + '@fastify/multipart': + specifier: ^9.2.1 + version: 9.2.1 axios: specifier: ^1.10.0 version: 1.10.0 @@ -47,6 +50,9 @@ importers: prom-client: specifier: ^15.1.3 version: 15.1.3 + sharp: + specifier: ^0.34.4 + version: 0.34.4 solhint: specifier: ^6.0.0 version: 6.0.0(typescript@5.8.3) @@ -88,6 +94,9 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@esbuild/aix-ppc64@0.25.6': resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} engines: {node: '>=18'} @@ -252,12 +261,18 @@ packages: '@fastify/ajv-compiler@4.0.2': resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==} + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + '@fastify/cookie@11.0.2': resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} '@fastify/deepmerge@2.0.2': resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==} + '@fastify/deepmerge@3.1.0': + resolution: {integrity: sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==} + '@fastify/env@5.0.2': resolution: {integrity: sha512-4m/jHS3s/G/DBJVODob9sxGUei/Ij8JFbA2PYqBfoihTm+Qqae2xD9xhez68UFZu1d4SNJPIb6uAOwbNvRYw+A==} @@ -276,6 +291,9 @@ packages: '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + '@fastify/multipart@9.2.1': + resolution: {integrity: sha512-U4221XDMfzCUtfzsyV1/PkR4MNgKI0158vUUyn/oF2Tl6RxMc+N7XYLr5fZXQiEC+Fmw5zFaTjxsTGTgtDtK+g==} + '@fastify/proxy-addr@5.0.0': resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==} @@ -283,6 +301,132 @@ packages: resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} engines: {node: '>=10.10.0'} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.4': + resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.4': + resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.3': + resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.3': + resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.3': + resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.3': + resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.3': + resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.3': + resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.3': + resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.4': + resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.4': + resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.4': + resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.4': + resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.4': + resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.4': + resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.4': + resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.4': + resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.4': + resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.4': + resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.4': + resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -753,6 +897,10 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dotenv-expand@10.0.0: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} @@ -1452,6 +1600,10 @@ packages: set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + sharp@0.34.4: + resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -1667,6 +1819,11 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.7.0 + optional: true + '@esbuild/aix-ppc64@0.25.6': optional: true @@ -1753,6 +1910,8 @@ snapshots: ajv-formats: 3.0.1(ajv@8.17.1) fast-uri: 3.0.6 + '@fastify/busboy@3.2.0': {} + '@fastify/cookie@11.0.2': dependencies: cookie: 1.0.2 @@ -1760,6 +1919,8 @@ snapshots: '@fastify/deepmerge@2.0.2': {} + '@fastify/deepmerge@3.1.0': {} + '@fastify/env@5.0.2': dependencies: env-schema: 6.0.1 @@ -1785,6 +1946,14 @@ snapshots: dependencies: dequal: 2.0.3 + '@fastify/multipart@9.2.1': + dependencies: + '@fastify/busboy': 3.2.0 + '@fastify/deepmerge': 3.1.0 + '@fastify/error': 4.2.0 + fastify-plugin: 5.0.1 + secure-json-parse: 4.0.0 + '@fastify/proxy-addr@5.0.0': dependencies: '@fastify/forwarded': 3.0.0 @@ -1792,6 +1961,94 @@ snapshots: '@humanwhocodes/momoa@2.0.4': {} + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.3 + optional: true + + '@img/sharp-darwin-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.3': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.3': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.3': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + optional: true + + '@img/sharp-linux-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.3 + optional: true + + '@img/sharp-linux-arm@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.3 + optional: true + + '@img/sharp-linux-ppc64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.3 + optional: true + + '@img/sharp-linux-s390x@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.3 + optional: true + + '@img/sharp-linux-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.3 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + optional: true + + '@img/sharp-wasm32@0.34.4': + dependencies: + '@emnapi/runtime': 1.5.0 + optional: true + + '@img/sharp-win32-arm64@0.34.4': + optional: true + + '@img/sharp-win32-ia32@0.34.4': + optional: true + + '@img/sharp-win32-x64@0.34.4': + optional: true + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -2183,6 +2440,8 @@ snapshots: detect-libc@2.0.4: {} + detect-libc@2.1.2: {} + dotenv-expand@10.0.0: {} dotenv@16.6.1: {} @@ -2914,6 +3173,35 @@ snapshots: set-cookie-parser@2.7.1: {} + sharp@0.34.4: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.4 + '@img/sharp-darwin-x64': 0.34.4 + '@img/sharp-libvips-darwin-arm64': 1.2.3 + '@img/sharp-libvips-darwin-x64': 1.2.3 + '@img/sharp-libvips-linux-arm': 1.2.3 + '@img/sharp-libvips-linux-arm64': 1.2.3 + '@img/sharp-libvips-linux-ppc64': 1.2.3 + '@img/sharp-libvips-linux-s390x': 1.2.3 + '@img/sharp-libvips-linux-x64': 1.2.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + '@img/sharp-linux-arm': 0.34.4 + '@img/sharp-linux-arm64': 0.34.4 + '@img/sharp-linux-ppc64': 0.34.4 + '@img/sharp-linux-s390x': 0.34.4 + '@img/sharp-linux-x64': 0.34.4 + '@img/sharp-linuxmusl-arm64': 0.34.4 + '@img/sharp-linuxmusl-x64': 0.34.4 + '@img/sharp-wasm32': 0.34.4 + '@img/sharp-win32-arm64': 0.34.4 + '@img/sharp-win32-ia32': 0.34.4 + '@img/sharp-win32-x64': 0.34.4 + simple-concat@1.0.1: {} simple-get@4.0.1: diff --git a/src/api/auth/remove.js b/src/api/auth/remove.js index 90579af..3f653c6 100644 --- a/src/api/auth/remove.js +++ b/src/api/auth/remove.js @@ -1,5 +1,5 @@ -import authDB from '../../utils/authDB'; -import { authUserRemove } from '../../utils/authUserRemove'; +import authDB from '../../utils/authDB.js'; +import { authUserRemove } from '../../utils/authUserRemove.js'; /** * @param {import('fastify').FastifyRequest} request diff --git a/src/api/images/dImage.js b/src/api/images/dImage.js new file mode 100644 index 0000000..5b18db7 --- /dev/null +++ b/src/api/images/dImage.js @@ -0,0 +1,10 @@ +export async function dImage(request, reply, fastify, deleteImage) { + try { + const imageId = Number(request.params.imageId); + deleteImage.run(imageId); + return reply.code(200).send({ msg: "Image deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/images/default.js b/src/api/images/default.js new file mode 100644 index 0000000..569be87 --- /dev/null +++ b/src/api/images/default.js @@ -0,0 +1,86 @@ +import fastifyJWT from '@fastify/jwt'; +import fastifyCookie from '@fastify/cookie'; +import Database from 'better-sqlite3'; +import multipart from '@fastify/multipart'; + +import { gImage } from './gImage.js'; +import { pImage } from './pImage.js'; +import { dImage } from './dImage.js'; + +const env = process.env.NODE_ENV || 'development'; + +let database; +if (!env || env === 'development') { + database = new Database(':memory:', { verbose: console.log }); +} else { + const dbPath = process.env.DB_PATH || '/db/db.sqlite' + database = new Database(dbPath); +} + +function prepareDB() { + database.exec(` + CREATE TABLE IF NOT EXISTS images ( + imageId INTEGER PRIMARY KEY AUTOINCREMENT, + fileName TEXT, + mimeType TEXT, + data BLOB + ) STRICT + `); +} + +prepareDB(); + +// POST +const postImage = database.prepare('INSERT INTO images (fileName, mimeType, data) VALUES (?, ?, ?);'); + +// GET +const getImage = database.prepare('SELECT fileName, mimeType, data FROM images WHERE imageId = ?;'); + +// DELETE +const deleteImage = database.prepare('DELETE FROM images WHERE imageId = ?;'); + +export default async function(fastify, options) { + fastify.register(fastifyJWT, { + secret: process.env.JWT_SECRET || '123456789101112131415161718192021', + cookie: { + cookieName: 'token', + }, + }); + fastify.register(fastifyCookie); + fastify.register(multipart, { limits: { fileSize: 2 * 1024 * 1024 } }); + + fastify.decorate('authenticate', async function(request, reply) { + try { + const jwt = await request.jwtVerify(); + request.user = jwt.user; + } catch (err) { + reply.code(401).send({ error: 'Unauthorized' }); + } + }); + + fastify.decorate('authenticateAdmin', async function(request, reply) { + try { + const jwt = await request.jwtVerify(); + if (jwt.user !== 'admin') { + throw ('You lack administrator privileges'); + } + } catch (err) { + reply.code(401).send({ error: 'Unauthorized' }); + } + }); + + // GET + fastify.get('/images/:imageId', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return gImage(request, reply, fastify, getImage); + }); + + // POST + fastify.post('/images', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return pImage(request, reply, fastify, postImage); + }); + + // DELETE + fastify.delete('/images/:imageId', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return dImage(request, reply, fastify, deleteImage); + }); +} diff --git a/src/api/images/gImage.js b/src/api/images/gImage.js new file mode 100644 index 0000000..abbaa42 --- /dev/null +++ b/src/api/images/gImage.js @@ -0,0 +1,13 @@ +export async function gImage(request, reply, fastify, getImage) { + try { + const imageId = Number(request.params.imageId); + const image = getImage.get(imageId); + if (!image) { + return reply.code(404).send({ error: "Image does not exist" }); + } + return reply.code(200).type(image.mimeType).header('Content-Disposition', `inline; filename="${image.fileName}"`).send(image.data); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/images/pImage.js b/src/api/images/pImage.js new file mode 100644 index 0000000..d3b930b --- /dev/null +++ b/src/api/images/pImage.js @@ -0,0 +1,33 @@ +export async function pImage(request, reply, fastify, postImage) { + try { + const parts = request.parts(); + for await (const part of parts) { + if (part.file) { + const chunks = []; + for await (const chunk of part.file) { + chunks.push(chunk); + } + const buffer = Buffer.concat(chunks); + if (!part.filename || part.filename.trim() === '') { + return reply.code(400).send({ error: "Missing filename" }); + } + if (!part.mimetype || part.mimetype.trim() === '') { + return reply.code(400).send({ error: "Missing mimetype" }); + } + const ext = part.filename.toLowerCase().substring(part.filename.lastIndexOf('.')); + if (ext !== 'webp') { + return reply.code(400).send({ error: "Wrong file extension" }); + } + // check size max here ? + // convert image to webp using sharp + //sharp(buffer, ).toFile(); + const id = postImage.run(part.filename, part.mimetype, buffer); + return reply.code(200).send({ msg: "Image uploaded successfully", imageId: id.lastInsertRowid }); + } + } + return reply.code(400).send({ error: "No file uploaded" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/TODO b/src/api/user/TODO deleted file mode 100644 index 326da16..0000000 --- a/src/api/user/TODO +++ /dev/null @@ -1,8 +0,0 @@ -Todo : -- create users with an avatar (by default) -> POST/GET/PATCH/DELETE avatar -- create a whole image upload API that ensures files are not executables, converts to a single type, stores the image and returns a UID to address them -- add a privacy setting so not anybody can GET friends, match history, etc. (what are the RGPD requirements ?) ? - - - -Always update API doc diff --git a/src/api/user/dAvatar.js b/src/api/user/dAvatar.js new file mode 100644 index 0000000..822c786 --- /dev/null +++ b/src/api/user/dAvatar.js @@ -0,0 +1,9 @@ +export async function dAvatar(request, reply, fastify, deleteAvatarId) { + try { + ; + return reply.code(200).send({ msg: "Avatar deleted successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/default.js b/src/api/user/default.js index a2cc806..1b57c07 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -2,22 +2,25 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; import Database from 'better-sqlite3'; -import { gUsers } from './gUsers.js' -import { gUser } from './gUser.js' -import { gNumberUsers } from './gNumberUsers.js' -import { gFriends } from './gFriends.js' -import { gNumberFriends } from './gNumberFriends.js' -import { gMatchHistory } from './gMatchHistory.js' -import { gNumberMatches } from './gNumberMatches.js' -import { pUser } from './pUser.js' -import { pFriend } from './pFriend.js' -import { pMatchHistory } from './pMatchHistory.js' -import { uMember } from './uMember.js' -import { dUser } from './dUser.js' -import { dMember } from './dMember.js' -import { dFriends } from './dFriends.js' -import { dFriend } from './dFriend.js' -import { dMatchHistory } from './dMatchHistory.js' +import { gUsers } from './gUsers.js'; +import { gUser } from './gUser.js'; +import { gNumberUsers } from './gNumberUsers.js'; +import { gFriends } from './gFriends.js'; +import { gNumberFriends } from './gNumberFriends.js'; +import { gMatchHistory } from './gMatchHistory.js'; +import { gNumberMatches } from './gNumberMatches.js'; +import { pUser } from './pUser.js'; +import { pFriend } from './pFriend.js'; +import { pMatchHistory } from './pMatchHistory.js'; +import { uMember } from './uMember.js'; +import { dUser } from './dUser.js'; +import { dMember } from './dMember.js'; +import { dFriends } from './dFriends.js'; +import { dFriend } from './dFriend.js'; +import { dMatchHistory } from './dMatchHistory.js'; +import { pAvatar } from './pAvatar.js'; +import { gAvatar } from './gAvatar.js'; +import { dAvatar } from './dAvatar.js'; const env = process.env.NODE_ENV || 'development'; @@ -35,6 +38,7 @@ function prepareDB() { id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, displayName TEXT, + avatarId INTEGER, pongWins INTEGER, pongLosses INTEGER, tetrisWins INTEGER, @@ -73,16 +77,18 @@ function prepareDB() { prepareDB(); // POST -const createUser = database.prepare('INSERT INTO userData (username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses) VALUES (?, ?, 0, 0, 0, 0);'); +const createUser = database.prepare('INSERT INTO userData (username, displayName, avatarId, pongWins, pongLosses, tetrisWins, tetrisLosses) VALUES (?, ?, -1, 0, 0, 0, 0);'); const addFriend = database.prepare('INSERT INTO friends (username, friendName) VALUES (?, ?);'); const addMatch = database.prepare('INSERT INTO matchHistory (game, date, player1, player2, matchId) VALUES (?, ?, ?, ?, ?);'); const incWinsPong = database.prepare('UPDATE userData SET pongWins = pongWins + 1 WHERE username = ?;'); const incLossesPong = database.prepare('UPDATE userData SET pongLosses = pongLosses + 1 WHERE username = ?'); const incWinsTetris = database.prepare('UPDATE userData SET tetrisWins = tetrisWins + 1 WHERE username = ?;'); const incLossesTetris = database.prepare('UPDATE userData SET tetrisLosses = tetrisLosses + 1 WHERE username = ?'); +const setAvatarId = database.prepare('UPDATE userData SET avatarId = ? WHERE username = ?;'); // PATCH const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;'); +const changeAvatarId = database.prepare('UPDATE userData SET avatarId = ? WHERE username = ?;'); // GET const getUserData = database.prepare('SELECT username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses FROM userData LIMIT ? OFFSET ?;'); @@ -92,7 +98,8 @@ const getFriend = database.prepare('SELECT friendName FROM friends WHERE usernam const getMatchHistory = database.prepare('SELECT matchId, date FROM matchHistory WHERE game = ? AND ? IN (player1, player2) LIMIT ? OFFSET ?;'); const getNumberUsers = database.prepare('SELECT COUNT (DISTINCT username) AS n_users FROM userData;'); const getNumberFriends = database.prepare('SELECT COUNT (DISTINCT friendName) AS n_friends FROM friends WHERE username = ?;'); -const getNumberMatches = database.prepare('SELECT COUNT (DISTINCT id) AS n_matches FROM matchHistory WHERE game = ? AND ? IN (player1, player2);') +const getNumberMatches = database.prepare('SELECT COUNT (DISTINCT id) AS n_matches FROM matchHistory WHERE game = ? AND ? IN (player1, player2);'); +const getAvatarId = database.prepare('SELECT avatarId FROM userData WHERE username = ?;'); // DELETE const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;'); @@ -101,6 +108,7 @@ const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;' const deleteMatchHistory = database.prepare('DELETE FROM matchHistory WHERE game = ? AND ? IN (player1, player2);'); const deleteStatsPong = database.prepare('UPDATE userData SET pongWins = 0, pongLosses = 0 WHERE username = ?;'); const deleteStatsTetris = database.prepare('UPDATE userData SET tetrisWins = 0, tetrisLosses = 0 WHERE username = ?;'); +const deleteAvatarId = database.prepare('UPDATE userData SET avatarId = -1 WHERE username = ?;'); const querySchema = { type: 'object', required: ['iStart', 'iEnd'], properties: { iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } } const bodySchema = { type: 'object', required: ['opponent', 'myScore', 'opponentScore'], properties: { opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } } @@ -159,6 +167,9 @@ export default async function(fastify, options) { fastify.get('/users/:userId/matchHistory/count', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => { return gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches); }); + fastify.get('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return gAvatar(request, reply, fastify, getAvatarId); + }); // POST fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => { @@ -170,10 +181,13 @@ export default async function(fastify, options) { fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMatchHistory } }, async (request, reply) => { return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris); }); + fastify.post('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return pAvatar(request, reply, fastify, setAvatarId); + }); // PATCH fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate] }, async (request, reply) => { - return uMember(request, reply, fastify, getUserInfo, changeDisplayName); + return uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId); }); // DELETE @@ -192,4 +206,7 @@ export default async function(fastify, options) { fastify.delete('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => { return dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris); }); + fastify.delete('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { + return dAvatar(request, reply, fastify, deleteAvatarId); + }); } diff --git a/src/api/user/gAvatar.js b/src/api/user/gAvatar.js new file mode 100644 index 0000000..2323220 --- /dev/null +++ b/src/api/user/gAvatar.js @@ -0,0 +1,9 @@ +export async function gAvatar(request, reply, fastify, getAvatarId) { + try { + ; + return reply.code(200).send({ }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pAvatar.js b/src/api/user/pAvatar.js new file mode 100644 index 0000000..e7e9db6 --- /dev/null +++ b/src/api/user/pAvatar.js @@ -0,0 +1,13 @@ +export async function pAvatar(request, reply, fastify, setAvatarId) { + try { +/* const res = await fetch('http://localhost:3004/images', { method: "POST", headers: { } }); + if (!res.ok) { + return reply.code(500).send({ error: "Internal server error" }); + } + const data = await res.json();*/ + return reply.code(200).send({ msg: "Avatar uploaded successfully" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/uMember.js b/src/api/user/uMember.js index 67997d7..01c1dc5 100644 --- a/src/api/user/uMember.js +++ b/src/api/user/uMember.js @@ -1,4 +1,4 @@ -export async function uMember(request, reply, fastify, getUserInfo, changeDisplayName) { +export async function uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId) { try { const userId = request.params.userId; if (!request.user) { diff --git a/src/start.js b/src/start.js index df7bc0e..c99534d 100644 --- a/src/start.js +++ b/src/start.js @@ -1,6 +1,7 @@ import Fastify from 'fastify'; import authApi from './api/auth/default.js'; import userApi from './api/user/default.js'; +import imagesApi from './api/images/default.js'; import scoreApi from './api/scoreStore/default.js'; import fs from 'fs'; import path from 'path'; @@ -68,6 +69,16 @@ async function start() { servers.push(score); } + if (target === 'images' || target === 'all') { + const images = Fastify({ logger: loggerOption('images') }); + images.register(imagesApi); + const port = target === 'all' ? 3004 : 3000; + const host = target === 'all' ? '127.0.0.1' : '0.0.0.0'; + await images.listen({ port, host }); + console.log(`Images API listening on http://${host}:${port}`); + servers.push(images); + } + // Graceful shutdown on SIGINT process.on('SIGINT', async () => { console.log('SIGINT received, closing servers...'); From f63aec4b9c8c23d59d42b004cd394dffbaff4725 Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Sat, 18 Oct 2025 07:05:49 +0200 Subject: [PATCH 17/45] updated todo --- src/api/TODO | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/api/TODO diff --git a/src/api/TODO b/src/api/TODO new file mode 100644 index 0000000..0b8e16c --- /dev/null +++ b/src/api/TODO @@ -0,0 +1,21 @@ +Todo : +- view friend's activity status +- add a privacy setting so not anybody can GET friends, match history, etc. (what are the RGPD requirements ?) ? +- test updated score storing (delays) + + + +User API : +POST /users/:userId/avatar + {image} (calls POST /images, stores returned imageId in DB at :userId) +GET /users/:userId/avatar -> {image} +DELETE /users/:userId/avatar (calls DELETE /images/:imageId, clears imageId in DB at :userId) +PATCH /users/:userId/avatar + {image} (emulates a DELETE /users/:userId/avatar and a POST /users/:userId/avatar) -> use uMember instead ? +- provide a random kanel image from Adam's website ? + +Image API : +- convert everything to webp (using @fastify/sharp) +- test size max + + + +Always update API docs From 62aaa32b9da81049ca563507261b59ad8ee80dd7 Mon Sep 17 00:00:00 2001 From: adjoly Date: Sun, 19 Oct 2025 16:42:00 +0200 Subject: [PATCH 18/45] =?UTF-8?q?=E3=80=8C=E2=9C=A8=E3=80=8D=20feat:=20add?= =?UTF-8?q?ed=20ping=20routes=20for=20activity=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/user/default.js | 20 ++++++++++++++++++++ src/api/user/gPing.js | 21 +++++++++++++++++++++ src/api/user/pPing.js | 21 +++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/api/user/gPing.js create mode 100644 src/api/user/pPing.js diff --git a/src/api/user/default.js b/src/api/user/default.js index 1b57c07..cef0c30 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -21,6 +21,8 @@ import { dMatchHistory } from './dMatchHistory.js'; import { pAvatar } from './pAvatar.js'; import { gAvatar } from './gAvatar.js'; import { dAvatar } from './dAvatar.js'; +import { pPing } from './pPing.js'; +import { gPing } from './gPing.js'; const env = process.env.NODE_ENV || 'development'; @@ -72,6 +74,12 @@ function prepareDB() { CHECK(date >= 0) ) STRICT `); + database.exec(` + CREATE TABLE IF NOT EXISTS activityTime ( + username TEXT PRIMARY KEY, + time TEXT + ) STRICT + `); } prepareDB(); @@ -85,6 +93,11 @@ const incLossesPong = database.prepare('UPDATE userData SET pongLosses = pongLos 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 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 = ?;'); @@ -100,6 +113,7 @@ const getNumberUsers = database.prepare('SELECT COUNT (DISTINCT username) AS n_u 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 getActivityTime = database.prepare('SELECT time FROM activityTime WHERE username = ?;') // DELETE const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;'); @@ -170,6 +184,9 @@ export default async function(fastify, options) { fastify.get('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { return gAvatar(request, reply, fastify, getAvatarId); }); + 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) => { @@ -184,6 +201,9 @@ export default async function(fastify, options) { fastify.post('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { return pAvatar(request, reply, fastify, setAvatarId); }); + 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) => { diff --git a/src/api/user/gPing.js b/src/api/user/gPing.js new file mode 100644 index 0000000..2d2d018 --- /dev/null +++ b/src/api/user/gPing.js @@ -0,0 +1,21 @@ +/** + * @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); + console.log(time) + + return reply.code(200) + .send({ + lastSeenTime: time.time + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/user/pPing.js b/src/api/user/pPing.js new file mode 100644 index 0000000..b7ff9fd --- /dev/null +++ b/src/api/user/pPing.js @@ -0,0 +1,21 @@ +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} request + * @param {import('fastify').Fastify} fastify + */ +export async function pPing(request, reply, fastify, setActivityTime) { + try { + const user = request.user; + const currentTime = new Date().toISOString(); + + setActivityTime.run(user, currentTime); + + return reply.code(200) + .send({ + msg: "last seen time updated successfully" + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} From 2757c5796d1b9cb6111d0ce95fe021e5f4a203cc Mon Sep 17 00:00:00 2001 From: adjoly Date: Sun, 19 Oct 2025 16:47:24 +0200 Subject: [PATCH 19/45] =?UTF-8?q?=E3=80=8C=F0=9F=93=9D=E3=80=8D=20doc:=20a?= =?UTF-8?q?dded=20doc=20to=20this?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/user/ping.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 doc/user/ping.md diff --git a/doc/user/ping.md b/doc/user/ping.md new file mode 100644 index 0000000..8051bf1 --- /dev/null +++ b/doc/user/ping.md @@ -0,0 +1,41 @@ +# ping + +Available endpoints: +- POST `/ping` +- GET `/ping/:userId` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/ping` + +Used to send a ping and update the lastSeenTime (can be used for activity time) + +Input needed : just need a valid token + +Can return: +- 200 +```json +{ + "msg": "last seen time updated successfully" +} +``` + +## GET `/ping/:userId` + +Used to retrive the lastSeenTime of a user + +Input needed : just need a valid token + +Can return: +- 200 +```json +{ + "lastSeenTime": "" +} +``` From c4403de09966313e1c72e2d0d8a68c62a51092e2 Mon Sep 17 00:00:00 2001 From: adjoly Date: Sun, 19 Oct 2025 17:02:27 +0200 Subject: [PATCH 20/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20c?= =?UTF-8?q?hange=20to=20isLogged=20boolean?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/user/ping.md | 2 +- src/api/user/gPing.js | 37 ++++++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/doc/user/ping.md b/doc/user/ping.md index 8051bf1..1e6dded 100644 --- a/doc/user/ping.md +++ b/doc/user/ping.md @@ -36,6 +36,6 @@ Can return: - 200 ```json { - "lastSeenTime": "" + "isLogged": "" } ``` diff --git a/src/api/user/gPing.js b/src/api/user/gPing.js index 2d2d018..b14f003 100644 --- a/src/api/user/gPing.js +++ b/src/api/user/gPing.js @@ -1,21 +1,28 @@ /** - * @param {import('fastify').FastifyRequest} request - * @param {import('fastify').FastifyReply} reply - * @param {import('fastify').FastifyInstance} fastify + * @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; + try { + const user = request.params.userId; + const time = getActivityTime.get(user); - const time = getActivityTime.get(user); - console.log(time) + if (!time || !time.time) { + return reply.code(404).send({ error: "User not found or no activity time recorded" }); + } - return reply.code(200) - .send({ - lastSeenTime: time.time - }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } + 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" }); + } } From 3a9d726f577adf4138a810f687c5498598ac1456 Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Sun, 19 Oct 2025 22:02:01 +0200 Subject: [PATCH 21/45] cleaned doc & friends provided with display names --- doc/user/friend.md | 155 ++++++++++++++++++++++ doc/user/matchHistory.md | 148 +++++++++++++++++++++ doc/user/user.md | 280 --------------------------------------- src/api/user/gFriends.js | 10 +- 4 files changed, 311 insertions(+), 282 deletions(-) create mode 100644 doc/user/friend.md create mode 100644 doc/user/matchHistory.md diff --git a/doc/user/friend.md b/doc/user/friend.md new file mode 100644 index 0000000..377af5c --- /dev/null +++ b/doc/user/friend.md @@ -0,0 +1,155 @@ +# Friend + +Available endpoints: +- POST `/users/:userId/friends` +- GET `/users/:userId/friends` +- GET `/users/:userId/friends/count` +- DELETE `/users/:userId/friends` +- DELETE `/users/:userId/friends/:friendId` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/users/:userId/friends/:friendId` + +Used to add a friend to an user + +Can return: +- 200 with response +```json +{ + "msg": "Friend added successfully" +} +``` +- 400 with response (if no user is specified in header, or friend is the user specified in header, or friend is already added) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or friend does not exist) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/friends?iStart=&iEnd=` + +Used to get the friends of an user + +Can return: +- 200 with response (list of friend objects (between iStart and iEnd)) +```json +{ + "friends": + [ + { + "friendName": "", + "friendDisplayName": "" + }, + ... + ] +} +``` +- 400 with response (if iStart/iEnd is missing, or iEnd < iStart) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or no friends exist in the selected range) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/friends/count` + +Used to get the number of friends of an user + +Can return: +- 200 with response +```json +{ + "n_friends": +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/friends` + +Used to delete the friends of an user + +Can return: +- 200 with response +```json +{ + "msg": "Friends deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/friends/:friendId` + +Used to delete a friend of an user + +Can return: +- 200 with response +```json +{ + "msg": "Friend deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or friend does not exist) +```json +{ + "error": "" +} +``` diff --git a/doc/user/matchHistory.md b/doc/user/matchHistory.md new file mode 100644 index 0000000..9293169 --- /dev/null +++ b/doc/user/matchHistory.md @@ -0,0 +1,148 @@ +# Match History + +Available endpoints: +- POST `/users/:userId/matchHistory` +- GET `/users/:userId/matchHistory` +- GET `/users/:userId/matchHistory/count` +- DELETE `/users/:userId/matchHistory` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST `/users/:userId/matchHistory?game=` + +Used to add a match result to an user to a specific game + +Input needed : +```json +{ + "game": "" + "opponent": "", + "myScore": , + "opponentScore": , + "date": +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Match successfully saved to the blockchain" +} +``` +- 400 with response (if no user is specified in header, or no opponent/p1Score/p2Score is specified in body, or opponent is the user specified in header, or a score is negative, or the game specified is invalid) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or opponent does not exist) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/matchHistory?game=&iStart=&iEnd=` + +Used to get the match history of an user for a specific game + +Can return: +- 200 with response (list of matches results (between iStart and iEnd)) +```json +{ + "matchHistory": + [ + { + "score": + { + "p1": "", + "p2": "", + "p1Score": "", + "p2Score": "", + "date": + }, + "tx": "" + }, + ... + ] +} +``` +- 400 with response (if iStart/iEnd does not exist, or iEnd < iStart, or the game specified is invalid) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist, or no matches exist in the selected range) +```json +{ + "error": "" +} +``` + +## GET `/users/:userId/matchHistory/count?game=` + +Used to get the number of matches an user played for a specific game + +Can return: +- 200 with response +```json +{ + "n_matches": +} +``` +- 400 with response (if game does not exist) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE `/users/:userId/matchHistory?game=` + +Used to delete the match history of an user for a specific game + +Can return: +- 200 with response +```json +{ + "msg": "Match history deleted successfully" +} +``` +- 400 with response (if user specified in header is neither admin nor user, or the game specified is invalid) +```json +{ + "error": "" +} +``` +- 401 with response (if user specified in header is neither admin nor user) +```json +{ + "error": "" +} +``` +- 404 with response (if user does not exist) +```json +{ + "error": "" +} +``` diff --git a/doc/user/user.md b/doc/user/user.md index d982e68..1f57d58 100644 --- a/doc/user/user.md +++ b/doc/user/user.md @@ -2,21 +2,12 @@ Available endpoints: - POST `/users/:userId` -- POST `/users/:userId/friends` -- POST `/users/:userId/matchHistory` - GET `/users` - GET `/users/count` - GET `/users/:userId` -- GET `/users/:userId/friends` -- GET `/users/:userId/friends/count` -- GET `/users/:userId/matchHistory` -- GET `/users/:userId/matchHistory/count` - PATCH `/users/:userId/:member` - DELETE `/users/:userId` - DELETE `/users/:userId/:member` -- DELETE `/users/:userId/friends` -- DELETE `/users/:userId/friends/:friendId` -- DELETE `/users/:userId/matchHistory` Common return: - 500 with response @@ -57,77 +48,6 @@ Can return: } ``` -## POST `/users/:userId/friends/:friendId` - -Used to add a friend to an user - -Can return: -- 200 with response -```json -{ - "msg": "Friend added successfully" -} -``` -- 400 with response (if no user is specified in header, or friend is the user specified in header, or friend is already added) -```json -{ - "error": "" -} -``` -- 404 with response (if user does not exist, or friend does not exist) -```json -{ - "error": "" -} -``` -- 401 with response (if user specified in header is neither admin nor user) -```json -{ - "error": "" -} -``` - -## POST `/users/:userId/matchHistory?game=` - -Used to add a match result to an user to a specific game - -Input needed : -```json -{ - "game": "" - "opponent": "", - "myScore": , - "opponentScore": , - "date": -} -``` - -Can return: -- 200 with response -```json -{ - "msg": "Match successfully saved to the blockchain" -} -``` -- 400 with response (if no user is specified in header, or no opponent/p1Score/p2Score is specified in body, or opponent is the user specified in header, or a score is negative, or the game specified is invalid) -```json -{ - "error": "" -} -``` -- 404 with response (if user does not exist, or opponent does not exist) -```json -{ - "error": "" -} -``` -- 401 with response (if user specified in header is neither admin nor user) -```json -{ - "error": "" -} -``` - ## GET `/users?iStart=&iEnd=` Used to get the list of users @@ -206,116 +126,6 @@ Can return: } ``` -## GET `/users/:userId/friends?iStart=&iEnd=` - -Used to get the friends of an user - -Can return: -- 200 with response (list of friend objects (between iStart and iEnd)) -```json -{ - "friends": - [ - { - "friendName": "" - }, - ... - ] -} -``` -- 400 with response (if iStart/iEnd is missing, or iEnd < iStart) -```json -{ - "error": "" -} -``` -- 404 with response (if user does not exist, or no friends exist in the selected range) -```json -{ - "error": "" -} -``` - -## GET `/users/:userId/friends/count` - -Used to get the number of friends of an user - -Can return: -- 200 with response -```json -{ - "n_friends": -} -``` -- 404 with response (if user does not exist) -```json -{ - "error": "" -} -``` - -## GET `/users/:userId/matchHistory?game=&iStart=&iEnd=` - -Used to get the match history of an user for a specific game - -Can return: -- 200 with response (list of matches results (between iStart and iEnd)) -```json -{ - "matchHistory": - [ - { - "score": - { - "p1": "", - "p2": "", - "p1Score": "", - "p2Score": "", - "date": - }, - "tx": "" - }, - ... - ] -} -``` -- 400 with response (if iStart/iEnd does not exist, or iEnd < iStart, or the game specified is invalid) -```json -{ - "error": "" -} -``` -- 404 with response (if user does not exist, or no matches exist in the selected range) -```json -{ - "error": "" -} -``` - -## GET `/users/:userId/matchHistory/count?game=` - -Used to get the number of matches an user played for a specific game - -Can return: -- 200 with response -```json -{ - "n_matches": -} -``` -- 400 with response (if game does not exist) -```json -{ - "error": "" -} -``` -- 404 with response (if user does not exist) -```json -{ - "error": "" -} -``` - ## PATCH `/users/:userId/:member` Used to modify a member of an user (only displayName can be modified) @@ -400,93 +210,3 @@ Can return: "error": "" } ``` - -## DELETE `/users/:userId/friends` - -Used to delete the friends of an user - -Can return: -- 200 with response -```json -{ - "msg": "Friends deleted successfully" -} -``` -- 400 with response (if user specified in header is neither admin nor user) -```json -{ - "error": "" -} -``` -- 401 with response (if user specified in header is neither admin nor user) -```json -{ - "error": "" -} -``` -- 404 with response (if user does not exist) -```json -{ - "error": "" -} -``` - -## DELETE `/users/:userId/friends/:friendId` - -Used to delete a friend of an user - -Can return: -- 200 with response -```json -{ - "msg": "Friend deleted successfully" -} -``` -- 400 with response (if user specified in header is neither admin nor user) -```json -{ - "error": "" -} -``` -- 401 with response (if user specified in header is neither admin nor user) -```json -{ - "error": "" -} -``` -- 404 with response (if user does not exist, or friend does not exist) -```json -{ - "error": "" -} -``` - -## DELETE `/users/:userId/matchHistory?game=` - -Used to delete the match history of an user for a specific game - -Can return: -- 200 with response -```json -{ - "msg": "Match history deleted successfully" -} -``` -- 400 with response (if user specified in header is neither admin nor user, or the game specified is invalid) -```json -{ - "error": "" -} -``` -- 401 with response (if user specified in header is neither admin nor user) -```json -{ - "error": "" -} -``` -- 404 with response (if user does not exist) -```json -{ - "error": "" -} -``` diff --git a/src/api/user/gFriends.js b/src/api/user/gFriends.js index cc24986..5fbb941 100644 --- a/src/api/user/gFriends.js +++ b/src/api/user/gFriends.js @@ -8,10 +8,16 @@ export async function gFriends(request, reply, fastify, getUserInfo, getFriends) if (Number(iEnd) < Number(iStart)) { return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" }); } - const friends = getFriends.all(userId, Number(iEnd) - Number(iStart), Number(iStart)); - if (!friends.length) { + 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); From a05aba6668c8e213d98c6de8a2b778bebee25879 Mon Sep 17 00:00:00 2001 From: Adam <45126464+KeyZox71@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:01:57 +0200 Subject: [PATCH 22/45] no rgpd --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b7c08ae..4406191 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Press F to pay respect - [ ] User and game stats dashboards - Minor - **Cybersecurity** - [ ] ~~WAF/ModSecurity and Hashicorp Vault - Major~~ - - [ ] RGPD compliance - Minor + - [ ] ~~RGPD compliance - Minor~~ - [x] 2FA and JWT - Major - **DevOps** - [x] Infrasctructure setup for log management - Major From 300e5a2b33b8dad33143653cae5ba57f2c511c85 Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Wed, 22 Oct 2025 00:20:35 +0200 Subject: [PATCH 23/45] fixed single-player tetris games --- doc/user/matchHistory.md | 10 +++++----- src/api/TODO | 21 --------------------- src/api/scoreStore/default.js | 2 +- src/api/user/dAvatar.js | 8 ++++++-- src/api/user/default.js | 13 ++++++------- src/api/user/gAvatar.js | 26 +++++++++++++++++++++++--- src/api/user/gMatchHistory.js | 4 ++++ src/api/user/pAvatar.js | 12 +++++++++--- src/api/user/pMatchHistory.js | 27 +++++++++++++++++++-------- 9 files changed, 73 insertions(+), 50 deletions(-) delete mode 100644 src/api/TODO diff --git a/doc/user/matchHistory.md b/doc/user/matchHistory.md index 9293169..6145a8d 100644 --- a/doc/user/matchHistory.md +++ b/doc/user/matchHistory.md @@ -22,9 +22,9 @@ Input needed : ```json { "game": "" - "opponent": "", + "opponent": "", <= item only present if the match involved 2 players "myScore": , - "opponentScore": , + "opponentScore": , <= item only present if the match involved 2 players "date": } ``` @@ -36,7 +36,7 @@ Can return: "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) +- 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": "" @@ -69,9 +69,9 @@ Can return: "score": { "p1": "", - "p2": "", + "p2": "", <= item only present if the match involved 2 players "p1Score": "", - "p2Score": "", + "p2Score": "", <= item only present if the match involved 2 players "date": }, "tx": "" diff --git a/src/api/TODO b/src/api/TODO deleted file mode 100644 index 0b8e16c..0000000 --- a/src/api/TODO +++ /dev/null @@ -1,21 +0,0 @@ -Todo : -- view friend's activity status -- add a privacy setting so not anybody can GET friends, match history, etc. (what are the RGPD requirements ?) ? -- test updated score storing (delays) - - - -User API : -POST /users/:userId/avatar + {image} (calls POST /images, stores returned imageId in DB at :userId) -GET /users/:userId/avatar -> {image} -DELETE /users/:userId/avatar (calls DELETE /images/:imageId, clears imageId in DB at :userId) -PATCH /users/:userId/avatar + {image} (emulates a DELETE /users/:userId/avatar and a POST /users/:userId/avatar) -> use uMember instead ? -- provide a random kanel image from Adam's website ? - -Image API : -- convert everything to webp (using @fastify/sharp) -- test size max - - - -Always update API docs diff --git a/src/api/scoreStore/default.js b/src/api/scoreStore/default.js index 954fcc8..c6b39ea 100644 --- a/src/api/scoreStore/default.js +++ b/src/api/scoreStore/default.js @@ -20,7 +20,7 @@ export default async function(fastify, options) { required: ['p1', 'p2', 'p1Score', 'p2Score'], properties: { p1: { type: 'string', minLength: 1 }, - p2: { type: 'string', minLength: 1 }, + p2: { type: 'string', minLength: 0 }, p1Score: { type: 'integer', minimum: 0 }, p2Score: { type: 'integer', minimum: 0 }, } diff --git a/src/api/user/dAvatar.js b/src/api/user/dAvatar.js index 822c786..56f9db4 100644 --- a/src/api/user/dAvatar.js +++ b/src/api/user/dAvatar.js @@ -1,6 +1,10 @@ -export async function dAvatar(request, reply, fastify, deleteAvatarId) { +export async function dAvatar(request, reply, fastify, getUserInfo, deleteAvatarId) { try { - ; + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.cose(404).send({ error: "User does not exist" }); + } + deleteAvatarId.run(userId); return reply.code(200).send({ msg: "Avatar deleted successfully" }); } catch (err) { fastify.log.error(err); diff --git a/src/api/user/default.js b/src/api/user/default.js index cef0c30..204e2c7 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -69,9 +69,9 @@ function prepareDB() { player1 TEXT, player2 TEXT, matchId INTEGER, - CHECK(player1 != player2), CHECK(game = 'pong' OR game = 'tetris'), - CHECK(date >= 0) + CHECK(date >= 0), + CHECK(player1 != player2) ) STRICT `); database.exec(` @@ -125,9 +125,8 @@ const deleteStatsTetris = database.prepare('UPDATE userData SET tetrisWins = 0, const deleteAvatarId = database.prepare('UPDATE userData SET avatarId = -1 WHERE username = ?;'); const querySchema = { type: 'object', required: ['iStart', 'iEnd'], properties: { iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } } -const bodySchema = { type: 'object', required: ['opponent', 'myScore', 'opponentScore'], properties: { opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } } 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', 'opponent', 'myScore', 'opponentScore'], properties: { game: { type: 'string' }, date: { type: 'integer', minimum: 0 }, opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { 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' } } } export default async function(fastify, options) { @@ -182,7 +181,7 @@ export default async function(fastify, options) { return gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches); }); fastify.get('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { - return gAvatar(request, reply, fastify, getAvatarId); + return gAvatar(request, reply, fastify, getUserInfo, getAvatarId); }); fastify.get('/ping/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => { return gPing(request, reply, fastify, getActivityTime); @@ -199,7 +198,7 @@ export default async function(fastify, options) { return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris); }); fastify.post('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { - return pAvatar(request, reply, fastify, setAvatarId); + return pAvatar(request, reply, fastify, getUserInfo, setAvatarId); }); fastify.post('/ping', { preHandler: [fastify.authenticate] }, async (request, reply) => { return pPing(request, reply, fastify, setActivityTime); @@ -227,6 +226,6 @@ export default async function(fastify, options) { return dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris); }); fastify.delete('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { - return dAvatar(request, reply, fastify, deleteAvatarId); + return dAvatar(request, reply, fastify, getUserInfo, deleteAvatarId); }); } diff --git a/src/api/user/gAvatar.js b/src/api/user/gAvatar.js index 2323220..362aa2f 100644 --- a/src/api/user/gAvatar.js +++ b/src/api/user/gAvatar.js @@ -1,7 +1,27 @@ -export async function gAvatar(request, reply, fastify, getAvatarId) { +export async function gAvatar(request, reply, fastify, getUserInfo, getAvatarId) { try { - ; - return reply.code(200).send({ }); + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.code(404).send({ error: "User does not exist" }); + } + const imageId = 1;//getAvatarId.get(userId); + if (imageId === -1) { + ;// return random kanel image + } + const res = await fetch(`http://localhost:3004/images/${imageId}`, { method: "GET" }); + if (!res.ok) { + console.log("====================================\nAn error on the image API has occured"); + return reply.code(500).send({ error: "Internal server error" }); + } + for (const [key, value] of res.headers) { + reply.header(key, value); + } + if (res.body) { + reply.code(res.statusCode).send(res.body); + } else { + reply.code(res.statusCode).send(); + } + //return reply.code(200).type(res.header).send(res.body); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); diff --git a/src/api/user/gMatchHistory.js b/src/api/user/gMatchHistory.js index 7531ca0..6999c9b 100644 --- a/src/api/user/gMatchHistory.js +++ b/src/api/user/gMatchHistory.js @@ -22,6 +22,10 @@ export async function gMatchHistory(request, reply, fastify, getUserInfo, getMat } 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); diff --git a/src/api/user/pAvatar.js b/src/api/user/pAvatar.js index e7e9db6..71fd6ca 100644 --- a/src/api/user/pAvatar.js +++ b/src/api/user/pAvatar.js @@ -1,10 +1,16 @@ -export async function pAvatar(request, reply, fastify, setAvatarId) { +export async function pAvatar(request, reply, fastify, getUserInfo, setAvatarId) { try { -/* const res = await fetch('http://localhost:3004/images', { method: "POST", headers: { } }); + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.cose(404).send({ error: "User does not exist" }); + } + console.log("====================================\n", request.headers);//========== + const res = await fetch('http://localhost:3004/images', { method: "POST", headers: { "Content-Type": "image/webp" }, body: request.body ? JSON.stringify(request.body) : undefined }); if (!res.ok) { return reply.code(500).send({ error: "Internal server error" }); } - const data = await res.json();*/ + const data = await res.json(); + setAvatarId.run(data.imageId, userId); return reply.code(200).send({ msg: "Avatar uploaded successfully" }); } catch (err) { fastify.log.error(err); diff --git a/src/api/user/pMatchHistory.js b/src/api/user/pMatchHistory.js index f22ef38..584b53e 100644 --- a/src/api/user/pMatchHistory.js +++ b/src/api/user/pMatchHistory.js @@ -1,10 +1,16 @@ async function fetchSave(request, reply, userId, addMatch) { - const res = await fetch('http://localhost:3003/', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ p1: userId, p2: request.body.opponent, p1Score: request.body.myScore, p2Score: request.body.opponentScore }) }); + let opponentName = ''; + let opponentScore = 0; + if (request.body.opponent && request.body.opponentScore) { + opponentName = request.body.opponent; + opponentScore = request.body.opponentScore; + } + const res = await fetch('http://localhost:3003/', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ p1: userId, p2: opponentName, p1Score: request.body.myScore, p2Score: opponentScore }) }); if (!res.ok) { throw new Error('Internal server error'); } const data = await res.json(); - addMatch.run(request.body.game, request.body.date, userId, request.body.opponent, data.id); + 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) { @@ -19,14 +25,19 @@ export async function pMatchHistory(request, reply, fastify, getUserInfo, addMat if (request.body.game !== 'pong' && request.body.game !== 'tetris') { return reply.code(400).send({ error: "Specified game does not exist" }); } + if (request.body.game === 'pong' && (!request.body.opponent || !request.body.opponentScore)) { + return reply.code(400).send({ error: "Game requires two players" }); + } if (!getUserInfo.get(userId)) { return reply.code(404).send({ error: "User does not exist" }); } - if (!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" }); + if (request.body.opponent) { + if (!getUserInfo.get(request.body.opponent)) { + return reply.code(404).send({ error: "Opponent does not exist" }); + } + if (request.body.opponent === userId) { + return reply.code(400).send({ error: "Do you have dementia ? You cannot have played a match against yourself gramps" }); + } } await fetchSave(request, reply, userId, addMatch); if (request.body.game === 'pong') { @@ -38,7 +49,7 @@ export async function pMatchHistory(request, reply, fastify, getUserInfo, addMat incLossesPong.run(userId); } } - else if (request.body.game === 'tetris') { + 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); From 86b08240eab689b5c99b8c4a2e91174eb84ee2c6 Mon Sep 17 00:00:00 2001 From: Adam <45126464+KeyZox71@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:50:05 +0200 Subject: [PATCH 24/45] Add contract directory for smart contract files --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4406191..24e7168 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Press F to pay respect │ └── volumes.yml # Docker volume definitions ├── src/ # Application source code │ ├── api/ # Backend logic (auth, user management) +│ ├── contract/ # Smart contract files │ ├── front/ # Frontend files │ └── utils/ # Utility modules (auth, TOTP, etc.) ├── flake.nix & flake.lock # Nix flake configuration From 6a08241493031f038f1917df3460449243a0179a Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Wed, 22 Oct 2025 19:46:25 +0200 Subject: [PATCH 25/45] merged image API in user API --- Justfile | 4 -- doc/user/avatar.md | 120 ++++++++++++++++++++++++++++++++++++++ src/api/images/dImage.js | 10 ---- src/api/images/default.js | 86 --------------------------- src/api/images/gImage.js | 13 ----- src/api/images/pImage.js | 33 ----------- src/api/user/dAvatar.js | 7 ++- src/api/user/default.js | 25 ++++++-- src/api/user/gAvatar.js | 25 +++----- src/api/user/pAvatar.js | 36 +++++++++--- src/api/user/uAvatar.js | 45 ++++++++++++++ src/start.js | 11 ---- 12 files changed, 228 insertions(+), 187 deletions(-) create mode 100644 doc/user/avatar.md delete mode 100644 src/api/images/dImage.js delete mode 100644 src/api/images/default.js delete mode 100644 src/api/images/gImage.js delete mode 100644 src/api/images/pImage.js create mode 100644 src/api/user/uAvatar.js diff --git a/Justfile b/Justfile index fe4ea0f..ad2eb32 100644 --- a/Justfile +++ b/Justfile @@ -11,10 +11,6 @@ set dotenv-load @user $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true": fastify start src/api/user/default.js -# For launching the images api -@images $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true": - fastify start src/api/images/default.js - @scoreStore $FASTIFY_LOG_LEVEL="info" $FASTIFY_PRETTY_LOGS="true": fastify start src/api/scoreStore/default.js diff --git a/doc/user/avatar.md b/doc/user/avatar.md new file mode 100644 index 0000000..d3232e4 --- /dev/null +++ b/doc/user/avatar.md @@ -0,0 +1,120 @@ +# Avatar + +Available endpoints: +- POST `/users/:userId/avatar` +- GET `/users/:userId/avatar` +- PATCH `/users/:userId/avatar` +- DELETE `/users/:userId/avatar` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## POST /users/:userId/avatar + +Used to upload an avatar + +Input needed : +```json +{ + +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Avatar uploaded successfully" +} +``` +- 400 with response (if the file is too large, or file is missing, or it is missing a file name, or it is missing a mime type) +```json +{ + "error": "" +} +``` +- 404 with response (if the user does not exist) +```json +{ + "error": "" +} +``` + +## GET /users/:userId/avatar + +Used to download an avatar + +Input needed : +```json +{ + +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Avatar uploaded successfully" +} +``` +- 404 with response (if the user does not exist, or the user does not have an assigned avatar, or the image does not exist) +```json +{ + "error": "" +} +``` + +## PATCH /users/:userId/avatar + +Used to modify an avatar + +Input needed : +```json +{ + +} +``` + +Can return: +- 200 with response +```json +{ + "msg": "Avatar modified successfully" +} +``` +- 400 with response (if the file is too large, or file is missing, or it is missing a file name, or it is missing a mime type) +```json +{ + "error": "" +} +``` +- 404 with response (if the user does not exist) +```json +{ + "error": "" +} +``` + +## DELETE /users/:userId/avatar + +Used to delete an avatar + +Can return: +- 200 with response +```json +{ + "msg": "Avatar deleted successfully" +} +``` +- 404 with response (if the user does not exist, or the user does not have an assigned avatar) +```json +{ + "error": "" +} +``` diff --git a/src/api/images/dImage.js b/src/api/images/dImage.js deleted file mode 100644 index 5b18db7..0000000 --- a/src/api/images/dImage.js +++ /dev/null @@ -1,10 +0,0 @@ -export async function dImage(request, reply, fastify, deleteImage) { - try { - const imageId = Number(request.params.imageId); - deleteImage.run(imageId); - return reply.code(200).send({ msg: "Image deleted successfully" }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } -} diff --git a/src/api/images/default.js b/src/api/images/default.js deleted file mode 100644 index 569be87..0000000 --- a/src/api/images/default.js +++ /dev/null @@ -1,86 +0,0 @@ -import fastifyJWT from '@fastify/jwt'; -import fastifyCookie from '@fastify/cookie'; -import Database from 'better-sqlite3'; -import multipart from '@fastify/multipart'; - -import { gImage } from './gImage.js'; -import { pImage } from './pImage.js'; -import { dImage } from './dImage.js'; - -const env = process.env.NODE_ENV || 'development'; - -let database; -if (!env || env === 'development') { - database = new Database(':memory:', { verbose: console.log }); -} else { - const dbPath = process.env.DB_PATH || '/db/db.sqlite' - database = new Database(dbPath); -} - -function prepareDB() { - database.exec(` - CREATE TABLE IF NOT EXISTS images ( - imageId INTEGER PRIMARY KEY AUTOINCREMENT, - fileName TEXT, - mimeType TEXT, - data BLOB - ) STRICT - `); -} - -prepareDB(); - -// POST -const postImage = database.prepare('INSERT INTO images (fileName, mimeType, data) VALUES (?, ?, ?);'); - -// GET -const getImage = database.prepare('SELECT fileName, mimeType, data FROM images WHERE imageId = ?;'); - -// DELETE -const deleteImage = database.prepare('DELETE FROM images WHERE imageId = ?;'); - -export default async function(fastify, options) { - fastify.register(fastifyJWT, { - secret: process.env.JWT_SECRET || '123456789101112131415161718192021', - cookie: { - cookieName: 'token', - }, - }); - fastify.register(fastifyCookie); - fastify.register(multipart, { limits: { fileSize: 2 * 1024 * 1024 } }); - - fastify.decorate('authenticate', async function(request, reply) { - try { - const jwt = await request.jwtVerify(); - request.user = jwt.user; - } catch (err) { - reply.code(401).send({ error: 'Unauthorized' }); - } - }); - - fastify.decorate('authenticateAdmin', async function(request, reply) { - try { - const jwt = await request.jwtVerify(); - if (jwt.user !== 'admin') { - throw ('You lack administrator privileges'); - } - } catch (err) { - reply.code(401).send({ error: 'Unauthorized' }); - } - }); - - // GET - fastify.get('/images/:imageId', { preHandler: [fastify.authenticate] }, async (request, reply) => { - return gImage(request, reply, fastify, getImage); - }); - - // POST - fastify.post('/images', { preHandler: [fastify.authenticate] }, async (request, reply) => { - return pImage(request, reply, fastify, postImage); - }); - - // DELETE - fastify.delete('/images/:imageId', { preHandler: [fastify.authenticate] }, async (request, reply) => { - return dImage(request, reply, fastify, deleteImage); - }); -} diff --git a/src/api/images/gImage.js b/src/api/images/gImage.js deleted file mode 100644 index abbaa42..0000000 --- a/src/api/images/gImage.js +++ /dev/null @@ -1,13 +0,0 @@ -export async function gImage(request, reply, fastify, getImage) { - try { - const imageId = Number(request.params.imageId); - const image = getImage.get(imageId); - if (!image) { - return reply.code(404).send({ error: "Image does not exist" }); - } - return reply.code(200).type(image.mimeType).header('Content-Disposition', `inline; filename="${image.fileName}"`).send(image.data); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } -} diff --git a/src/api/images/pImage.js b/src/api/images/pImage.js deleted file mode 100644 index d3b930b..0000000 --- a/src/api/images/pImage.js +++ /dev/null @@ -1,33 +0,0 @@ -export async function pImage(request, reply, fastify, postImage) { - try { - const parts = request.parts(); - for await (const part of parts) { - if (part.file) { - const chunks = []; - for await (const chunk of part.file) { - chunks.push(chunk); - } - const buffer = Buffer.concat(chunks); - if (!part.filename || part.filename.trim() === '') { - return reply.code(400).send({ error: "Missing filename" }); - } - if (!part.mimetype || part.mimetype.trim() === '') { - return reply.code(400).send({ error: "Missing mimetype" }); - } - const ext = part.filename.toLowerCase().substring(part.filename.lastIndexOf('.')); - if (ext !== 'webp') { - return reply.code(400).send({ error: "Wrong file extension" }); - } - // check size max here ? - // convert image to webp using sharp - //sharp(buffer, ).toFile(); - const id = postImage.run(part.filename, part.mimetype, buffer); - return reply.code(200).send({ msg: "Image uploaded successfully", imageId: id.lastInsertRowid }); - } - } - return reply.code(400).send({ error: "No file uploaded" }); - } catch (err) { - fastify.log.error(err); - return reply.code(500).send({ error: "Internal server error" }); - } -} diff --git a/src/api/user/dAvatar.js b/src/api/user/dAvatar.js index 56f9db4..dbd634a 100644 --- a/src/api/user/dAvatar.js +++ b/src/api/user/dAvatar.js @@ -1,9 +1,14 @@ -export async function dAvatar(request, reply, fastify, getUserInfo, deleteAvatarId) { +export async function dAvatar(request, reply, fastify, getUserInfo, getAvatarId, deleteAvatarId, deleteImage) { try { const userId = request.params.userId; 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) { diff --git a/src/api/user/default.js b/src/api/user/default.js index 204e2c7..bbce5ab 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -1,6 +1,7 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; import Database from 'better-sqlite3'; +import multipart from '@fastify/multipart'; import { gUsers } from './gUsers.js'; import { gUser } from './gUser.js'; @@ -20,6 +21,7 @@ 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'; @@ -80,6 +82,14 @@ function prepareDB() { time TEXT ) STRICT `); + database.exec(` + CREATE TABLE IF NOT EXISTS images ( + imageId INTEGER PRIMARY KEY AUTOINCREMENT, + fileName TEXT, + mimeType TEXT, + data BLOB + ) STRICT + `); } prepareDB(); @@ -93,6 +103,7 @@ const incLossesPong = database.prepare('UPDATE userData SET pongLosses = pongLos 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 (?, ?) @@ -113,6 +124,7 @@ const getNumberUsers = database.prepare('SELECT COUNT (DISTINCT username) AS n_u 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 @@ -123,6 +135,7 @@ const deleteMatchHistory = database.prepare('DELETE FROM matchHistory WHERE game 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 querySchemaMatchHistory = { type: 'object', required: ['game', 'iStart', 'iEnd'], properties: { game: { type: 'string' }, iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } } @@ -137,6 +150,7 @@ export default async function(fastify, options) { }, }); fastify.register(fastifyCookie); + fastify.register(multipart, { limits: { fileSize: 2 * 1024 * 1024 + 1 } }); fastify.decorate('authenticate', async function(request, reply) { try { @@ -181,7 +195,7 @@ export default async function(fastify, options) { 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); + return gAvatar(request, reply, fastify, getUserInfo, getAvatarId, getImage); }); fastify.get('/ping/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => { return gPing(request, reply, fastify, getActivityTime); @@ -197,14 +211,17 @@ export default async function(fastify, options) { fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMatchHistory } }, async (request, reply) => { return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris); }); - fastify.post('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { - return pAvatar(request, reply, fastify, getUserInfo, setAvatarId); + fastify.post('/users/:userId/avatar',/* { 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/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] }, async (request, reply) => { return uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId); }); @@ -226,6 +243,6 @@ export default async function(fastify, options) { 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, deleteAvatarId); + return dAvatar(request, reply, fastify, getUserInfo, getAvatarId, deleteAvatarId, deleteImage); }); } diff --git a/src/api/user/gAvatar.js b/src/api/user/gAvatar.js index 362aa2f..6c0d3f6 100644 --- a/src/api/user/gAvatar.js +++ b/src/api/user/gAvatar.js @@ -1,27 +1,18 @@ -export async function gAvatar(request, reply, fastify, getUserInfo, getAvatarId) { +export async function gAvatar(request, reply, fastify, getUserInfo, getAvatarId, getImage) { try { const userId = request.params.userId; if (!getUserInfo.get(userId)) { return reply.code(404).send({ error: "User does not exist" }); } - const imageId = 1;//getAvatarId.get(userId); - if (imageId === -1) { - ;// return random kanel image + const imageId = getAvatarId.get(userId); + if (imageId.avatarId === -1) { + return reply.code(404).send({ error: "User does not have an avatar" }); } - const res = await fetch(`http://localhost:3004/images/${imageId}`, { method: "GET" }); - if (!res.ok) { - console.log("====================================\nAn error on the image API has occured"); - return reply.code(500).send({ error: "Internal server error" }); + const image = getImage.get(imageId.avatarId); + if (!image) { + return reply.code(404).send({ error: "Avatar does not exist" }); } - for (const [key, value] of res.headers) { - reply.header(key, value); - } - if (res.body) { - reply.code(res.statusCode).send(res.body); - } else { - reply.code(res.statusCode).send(); - } - //return reply.code(200).type(res.header).send(res.body); + return reply.code(200).type(image.mimeType).send(image.data); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); diff --git a/src/api/user/pAvatar.js b/src/api/user/pAvatar.js index 71fd6ca..a49ef02 100644 --- a/src/api/user/pAvatar.js +++ b/src/api/user/pAvatar.js @@ -1,17 +1,37 @@ -export async function pAvatar(request, reply, fastify, getUserInfo, setAvatarId) { +import sharp from 'sharp'; + +export async function pAvatar(request, reply, fastify, getUserInfo, setAvatarId, postImage) { try { const userId = request.params.userId; if (!getUserInfo.get(userId)) { return reply.cose(404).send({ error: "User does not exist" }); } - console.log("====================================\n", request.headers);//========== - const res = await fetch('http://localhost:3004/images', { method: "POST", headers: { "Content-Type": "image/webp" }, body: request.body ? JSON.stringify(request.body) : undefined }); - if (!res.ok) { - return reply.code(500).send({ error: "Internal server error" }); + const parts = request.parts(); + for await (const part of parts) { + if (part.file) { + let size = 0; + const chunks = []; + for await (const chunk of part.file) { + size += chunk.length; + chunks.push(chunk); + } + if (size === 2 * 1024 * 1024 + 1) { + return reply.code(400).send({ error: "File too large" }); + } + const buffer = Buffer.concat(chunks); + if (!part.filename || part.filename.trim() === '') { + return reply.code(400).send({ error: "Missing filename" }); + } + if (!part.mimetype || part.mimetype.trim() === '') { + return reply.code(400).send({ error: "Missing mimetype" }); + } + const webpBuffer = await sharp(buffer).toFormat('webp').toBuffer(); + const imageId = postImage.run(part.filename, part.mimetype, webpBuffer); + setAvatarId.run(imageId.lastInsertRowid, userId); + return reply.code(200).send({ msg: "Avatar uploaded successfully" }); + } } - const data = await res.json(); - setAvatarId.run(data.imageId, userId); - return reply.code(200).send({ msg: "Avatar uploaded successfully" }); + return reply.code(400).send({ error: "No avatar uploaded" }); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); diff --git a/src/api/user/uAvatar.js b/src/api/user/uAvatar.js new file mode 100644 index 0000000..b5b8b72 --- /dev/null +++ b/src/api/user/uAvatar.js @@ -0,0 +1,45 @@ +import sharp from 'sharp'; + +export async function uAvatar(request, reply, fastify, getUserInfo, setAvatarId, getAvatarId, deleteAvatarId, postImage, deleteImage) { + try { + const userId = request.params.userId; + if (!getUserInfo.get(userId)) { + return reply.cose(404).send({ error: "User does not exist" }); + } + deleteAvatarId.run(userId); + const parts = request.parts(); + for await (const part of parts) { + if (part.file) { + let size = 0; + const chunks = []; + for await (const chunk of part.file) { + size += chunk.length; + chunks.push(chunk); + } + if (size === 2 * 1024 * 1024 + 1) { + return reply.code(400).send({ error: "File too large" }); + } + const buffer = Buffer.concat(chunks); + if (!part.filename || part.filename.trim() === '') { + return reply.code(400).send({ error: "Missing filename" }); + } + if (!part.mimetype || part.mimetype.trim() === '') { + return reply.code(400).send({ error: "Missing mimetype" }); + } + const webpBuffer = await sharp(buffer).toFormat('webp').toBuffer(); + const imageId = postImage.run(part.filename, part.mimetype, webpBuffer); + const oldImageId = getAvatarId.get(userId); + if (oldImageId.avatarId !== -1) { + deleteImage.run(oldImageId.avatarId); + deleteAvatarId.run(userId); + } + setAvatarId.run(imageId.lastInsertRowid, userId); + return reply.code(200).send({ msg: "Avatar modified successfully" }); + } + } + return reply.code(400).send({ error: "No avatar modified" }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/start.js b/src/start.js index c99534d..df7bc0e 100644 --- a/src/start.js +++ b/src/start.js @@ -1,7 +1,6 @@ import Fastify from 'fastify'; import authApi from './api/auth/default.js'; import userApi from './api/user/default.js'; -import imagesApi from './api/images/default.js'; import scoreApi from './api/scoreStore/default.js'; import fs from 'fs'; import path from 'path'; @@ -69,16 +68,6 @@ async function start() { servers.push(score); } - if (target === 'images' || target === 'all') { - const images = Fastify({ logger: loggerOption('images') }); - images.register(imagesApi); - const port = target === 'all' ? 3004 : 3000; - const host = target === 'all' ? '127.0.0.1' : '0.0.0.0'; - await images.listen({ port, host }); - console.log(`Images API listening on http://${host}:${port}`); - servers.push(images); - } - // Graceful shutdown on SIGINT process.on('SIGINT', async () => { console.log('SIGINT received, closing servers...'); From 469e761f8dfec42c131792e0d023161b879473b2 Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Wed, 22 Oct 2025 20:04:57 +0200 Subject: [PATCH 26/45] finished avatars --- src/api/user/default.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/api/user/default.js b/src/api/user/default.js index bbce5ab..a2bb2aa 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -137,10 +137,11 @@ const deleteStatsTetris = database.prepare('UPDATE userData SET tetrisWins = 0, 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 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' } } } +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' } } }; export default async function(fastify, options) { fastify.register(fastifyJWT, { @@ -211,7 +212,7 @@ export default async function(fastify, options) { fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMatchHistory } }, async (request, reply) => { return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris); }); - fastify.post('/users/:userId/avatar',/* { preHandler: [fastify.authenticate] },*/ async (request, reply) => { + fastify.post('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { return pAvatar(request, reply, fastify, getUserInfo, setAvatarId, postImage); }); fastify.post('/ping', { preHandler: [fastify.authenticate] }, async (request, reply) => { @@ -222,7 +223,7 @@ export default async function(fastify, options) { 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] }, async (request, reply) => { + fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMember } }, async (request, reply) => { return uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId); }); From c256dd833a1c223692ec34331e931e905e4c11d7 Mon Sep 17 00:00:00 2001 From: Tzvetan Trave Date: Wed, 22 Oct 2025 20:13:46 +0200 Subject: [PATCH 27/45] increased max pfp size --- src/api/user/pAvatar.js | 2 +- src/api/user/uAvatar.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/user/pAvatar.js b/src/api/user/pAvatar.js index a49ef02..5b9f3a9 100644 --- a/src/api/user/pAvatar.js +++ b/src/api/user/pAvatar.js @@ -15,7 +15,7 @@ export async function pAvatar(request, reply, fastify, getUserInfo, setAvatarId, size += chunk.length; chunks.push(chunk); } - if (size === 2 * 1024 * 1024 + 1) { + if (size === 5 * 1024 * 1024 + 1) { return reply.code(400).send({ error: "File too large" }); } const buffer = Buffer.concat(chunks); diff --git a/src/api/user/uAvatar.js b/src/api/user/uAvatar.js index b5b8b72..1076e44 100644 --- a/src/api/user/uAvatar.js +++ b/src/api/user/uAvatar.js @@ -16,7 +16,7 @@ export async function uAvatar(request, reply, fastify, getUserInfo, setAvatarId, size += chunk.length; chunks.push(chunk); } - if (size === 2 * 1024 * 1024 + 1) { + if (size === 5 * 1024 * 1024 + 1) { return reply.code(400).send({ error: "File too large" }); } const buffer = Buffer.concat(chunks); From 5bbda2ab816a4394b12cc622df8d151bb5750a9d Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 13:20:13 +0200 Subject: [PATCH 28/45] =?UTF-8?q?=E3=80=8C=E2=9C=A8=E3=80=8D=20feat:=20add?= =?UTF-8?q?ed=202fa=20to=20login=20page=20:D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/static/ts/views/LoginPage.ts | 97 +++++++++++++++++++++++++- src/front/static/ts/views/Profile.ts | 1 + 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/front/static/ts/views/LoginPage.ts b/src/front/static/ts/views/LoginPage.ts index 0ed05c1..a7159cb 100644 --- a/src/front/static/ts/views/LoginPage.ts +++ b/src/front/static/ts/views/LoginPage.ts @@ -53,7 +53,40 @@ export default class extends Aview { } async run() { - dragElement(document.getElementById("window")); + dragElement(document.getElementById("window")); + + const totpVerify = async () => { + const username = (document.getElementById("username") as HTMLInputElement).value; + const password = (document.getElementById("password") as HTMLInputElement).value; + const totpPin = (document.getElementById('totpPin') as HTMLInputElement).value; + try { + const data_req = await fetch("http://localhost:3001/login", { + method: "POST", + headers: { "Content-Type": "application/json", }, + credentials: "include", + body: JSON.stringify({ user: username, password: password, token: totpPin }), + }); + if (data_req.status === 200) { + isLogged(); + navigationManager("/"); + } else if (data_req.status === 401) { + const data = await data_req.json(); + + const error = document.createElement("p"); + error.innerHTML = data.error; + error.classList.add("text-red-700", "dark:text-red-500"); + + idWindow.appendChild(error); + } else { + console.log(data_req.status) + console.log(await data_req.json()) + // throw new Error("invalid response"); + } + } catch (error) { + console.error(error); + } + } + const login = async () => { const username = (document.getElementById("username") as HTMLInputElement).value; const password = (document.getElementById("password") as HTMLInputElement).value; @@ -71,11 +104,69 @@ export default class extends Aview { 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"); + document.getElementById("login-error-message").innerHTML = "error: " + data.error; + document.getElementById("login-error-message").classList.remove("hidden"); } else { diff --git a/src/front/static/ts/views/Profile.ts b/src/front/static/ts/views/Profile.ts index b80bd0b..fda349f 100644 --- a/src/front/static/ts/views/Profile.ts +++ b/src/front/static/ts/views/Profile.ts @@ -127,6 +127,7 @@ export default class extends Aview { if (!main) return console.error("what"); + // don't read this shit for you mental health if (matches.matchHistory) { for (let match of matches.matchHistory) { const newEntry = document.createElement("li"); From 8da2193d9ef786974bf78ecb0f3c575f37752777 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 14:15:54 +0200 Subject: [PATCH 29/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20w?= =?UTF-8?q?orking=20error=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/static/ts/views/LoginPage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/front/static/ts/views/LoginPage.ts b/src/front/static/ts/views/LoginPage.ts index a7159cb..da0deec 100644 --- a/src/front/static/ts/views/LoginPage.ts +++ b/src/front/static/ts/views/LoginPage.ts @@ -59,6 +59,7 @@ export default class extends Aview { const username = (document.getElementById("username") as HTMLInputElement).value; const password = (document.getElementById("password") as HTMLInputElement).value; const totpPin = (document.getElementById('totpPin') as HTMLInputElement).value; + const idWindow = (document.getElementById('2fa-popup-content') as HTMLInputElement); try { const data_req = await fetch("http://localhost:3001/login", { method: "POST", From 182452e931c915418057fdf1092fc94037f087c9 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 14:16:31 +0200 Subject: [PATCH 30/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20n?= =?UTF-8?q?ow=20a=20good=20wallpaper=20for=20whilte=20theme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/front/index.html b/src/front/index.html index 8532da2..ee61724 100644 --- a/src/front/index.html +++ b/src/front/index.html @@ -12,7 +12,7 @@ - + From 494612a160007adb525e232c0d769c2c924f5b1e Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 14:54:53 +0200 Subject: [PATCH 31/45] =?UTF-8?q?=E3=80=8C=F0=9F=8F=97=EF=B8=8F=E3=80=8D?= =?UTF-8?q?=20wip:=20started=20adding=20enable=202fa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/static/ts/views/Settings.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/front/static/ts/views/Settings.ts b/src/front/static/ts/views/Settings.ts index 5c70167..94e6500 100644 --- a/src/front/static/ts/views/Settings.ts +++ b/src/front/static/ts/views/Settings.ts @@ -28,6 +28,8 @@ export default class extends Aview { +
+ `; @@ -80,5 +82,9 @@ export default class extends Aview { else console.error("xd"); // xd????????????? }); + + document.getElementById("2fa-button")?.addEventListener("click", async () => { + + }); } } From bcc8b2006ae6789eb51f023b06f1b0cb39908061 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 14:57:51 +0200 Subject: [PATCH 32/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix(auth?= =?UTF-8?q?api):=20remove=20should=20be=20working=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/authDB.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/authDB.js b/src/utils/authDB.js index a4f4595..4aee3c1 100644 --- a/src/utils/authDB.js +++ b/src/utils/authDB.js @@ -82,9 +82,16 @@ 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, From c4221d946348442eb8d6ce0441aae305345fc13f Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 15:22:47 +0200 Subject: [PATCH 33/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix(avat?= =?UTF-8?q?ar):=20fix=20this=20shit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 - pnpm-lock.yaml | 24 ------------------ pnpm-workspace.yaml | 1 + src/api/user/default.js | 9 +++++-- src/api/user/pAvatar.js | 55 ++++++++++++++++++++++------------------- src/utils/authDB.js | 1 - 6 files changed, 37 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 32cd1ca..797d338 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "@fastify/cookie": "^11.0.2", "@fastify/env": "^5.0.2", "@fastify/jwt": "^9.1.0", - "@fastify/multipart": "^9.2.1", "axios": "^1.10.0", "base32.js": "^0.1.0", "bcrypt": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c40ec77..41a1bb9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: '@fastify/jwt': specifier: ^9.1.0 version: 9.1.0 - '@fastify/multipart': - specifier: ^9.2.1 - version: 9.2.1 axios: specifier: ^1.10.0 version: 1.10.0 @@ -261,18 +258,12 @@ packages: '@fastify/ajv-compiler@4.0.2': resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==} - '@fastify/busboy@3.2.0': - resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} - '@fastify/cookie@11.0.2': resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} '@fastify/deepmerge@2.0.2': resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==} - '@fastify/deepmerge@3.1.0': - resolution: {integrity: sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==} - '@fastify/env@5.0.2': resolution: {integrity: sha512-4m/jHS3s/G/DBJVODob9sxGUei/Ij8JFbA2PYqBfoihTm+Qqae2xD9xhez68UFZu1d4SNJPIb6uAOwbNvRYw+A==} @@ -291,9 +282,6 @@ packages: '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} - '@fastify/multipart@9.2.1': - resolution: {integrity: sha512-U4221XDMfzCUtfzsyV1/PkR4MNgKI0158vUUyn/oF2Tl6RxMc+N7XYLr5fZXQiEC+Fmw5zFaTjxsTGTgtDtK+g==} - '@fastify/proxy-addr@5.0.0': resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==} @@ -1910,8 +1898,6 @@ snapshots: ajv-formats: 3.0.1(ajv@8.17.1) fast-uri: 3.0.6 - '@fastify/busboy@3.2.0': {} - '@fastify/cookie@11.0.2': dependencies: cookie: 1.0.2 @@ -1919,8 +1905,6 @@ snapshots: '@fastify/deepmerge@2.0.2': {} - '@fastify/deepmerge@3.1.0': {} - '@fastify/env@5.0.2': dependencies: env-schema: 6.0.1 @@ -1946,14 +1930,6 @@ snapshots: dependencies: dequal: 2.0.3 - '@fastify/multipart@9.2.1': - dependencies: - '@fastify/busboy': 3.2.0 - '@fastify/deepmerge': 3.1.0 - '@fastify/error': 4.2.0 - fastify-plugin: 5.0.1 - secure-json-parse: 4.0.0 - '@fastify/proxy-addr@5.0.0': dependencies: '@fastify/forwarded': 3.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a8eee40..8a8cca4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,3 +5,4 @@ ignoredBuiltDependencies: onlyBuiltDependencies: - better-sqlite3 + - sharp diff --git a/src/api/user/default.js b/src/api/user/default.js index a2bb2aa..79b5786 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -1,7 +1,6 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; import Database from 'better-sqlite3'; -import multipart from '@fastify/multipart'; import { gUsers } from './gUsers.js'; import { gUser } from './gUser.js'; @@ -151,7 +150,13 @@ export default async function(fastify, options) { }, }); fastify.register(fastifyCookie); - fastify.register(multipart, { limits: { fileSize: 2 * 1024 * 1024 + 1 } }); + + fastify.addContentTypeParser( + ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], + { parseAs: 'buffer' }, + async (request, payload) => payload + ); + fastify.decorate('authenticate', async function(request, reply) { try { diff --git a/src/api/user/pAvatar.js b/src/api/user/pAvatar.js index 5b9f3a9..a567589 100644 --- a/src/api/user/pAvatar.js +++ b/src/api/user/pAvatar.js @@ -1,37 +1,40 @@ 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 (!getUserInfo.get(userId)) { - return reply.cose(404).send({ error: "User does not exist" }); + return reply.code(404).send({ error: "User does not exist" }); } - const parts = request.parts(); - for await (const part of parts) { - if (part.file) { - let size = 0; - const chunks = []; - for await (const chunk of part.file) { - size += chunk.length; - chunks.push(chunk); - } - if (size === 5 * 1024 * 1024 + 1) { - return reply.code(400).send({ error: "File too large" }); - } - const buffer = Buffer.concat(chunks); - if (!part.filename || part.filename.trim() === '') { - return reply.code(400).send({ error: "Missing filename" }); - } - if (!part.mimetype || part.mimetype.trim() === '') { - return reply.code(400).send({ error: "Missing mimetype" }); - } - const webpBuffer = await sharp(buffer).toFormat('webp').toBuffer(); - const imageId = postImage.run(part.filename, part.mimetype, webpBuffer); - setAvatarId.run(imageId.lastInsertRowid, userId); - return reply.code(200).send({ msg: "Avatar uploaded successfully" }); - } + + // Read the raw body as a Buffer + const buffer = request.body; + + if (!buffer) { + return reply.code(400).send({ error: "No file uploaded" }); } - return reply.code(400).send({ error: "No avatar uploaded" }); + + // Check file size (5MB limit) + if (buffer.length > 5 * 1024 * 1024) { + return reply.code(400).send({ error: "File too large" }); + } + + // Convert to WebP + const webpBuffer = await sharp(buffer).toFormat('webp').toBuffer(); + + // Save the image and update the user's avatar + const mimeType = request.headers['content-type']; + const fileName = `avatar_${userId}.webp`; + const imageId = postImage.run(fileName, mimeType, webpBuffer); + + setAvatarId.run(imageId.lastInsertRowid, userId); + + return reply.code(200).send({ msg: "Avatar uploaded successfully" }); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); diff --git a/src/utils/authDB.js b/src/utils/authDB.js index 4aee3c1..69d1bde 100644 --- a/src/utils/authDB.js +++ b/src/utils/authDB.js @@ -87,7 +87,6 @@ function rmUser(user) { stmt.run(user); } - const authDB = { prepareDB, checkUser, From 0c9f5950474678a4d4e0a25ec45cbb737e8f6f57 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 15:49:35 +0200 Subject: [PATCH 34/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix(user?= =?UTF-8?q?-management):=20fix=20the=20fact=20the=20no=20fucking=20route?= =?UTF-8?q?=20was=20protected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/user/dAvatar.js | 6 ++++++ src/api/user/dFriend.js | 6 +++--- src/api/user/dFriends.js | 9 +++------ src/api/user/dMatchHistory.js | 9 +++------ src/api/user/dMember.js | 17 +++++++---------- src/api/user/dUser.js | 6 +++++- src/api/user/default.js | 1 + src/api/user/gAvatar.js | 3 +++ src/api/user/gFriends.js | 3 +++ src/api/user/gMatchHistory.js | 3 +++ src/api/user/gNumberFriends.js | 4 ++++ src/api/user/gNumberMatches.js | 3 +++ src/api/user/gUser.js | 19 ++++++++++++++++--- src/api/user/pAvatar.js | 3 +++ src/api/user/pFriend.js | 7 ++----- src/api/user/pMatchHistory.js | 8 ++++---- src/api/user/pUser.js | 5 +---- src/api/user/uAvatar.js | 5 ++++- src/api/user/uMember.js | 7 ++----- 19 files changed, 76 insertions(+), 48 deletions(-) diff --git a/src/api/user/dAvatar.js b/src/api/user/dAvatar.js index dbd634a..cfbea1c 100644 --- a/src/api/user/dAvatar.js +++ b/src/api/user/dAvatar.js @@ -1,6 +1,12 @@ 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" }); } diff --git a/src/api/user/dFriend.js b/src/api/user/dFriend.js index 333bd93..1f3fddd 100644 --- a/src/api/user/dFriend.js +++ b/src/api/user/dFriend.js @@ -1,9 +1,9 @@ export async function dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend) { try { - if (!request.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } 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" }); } diff --git a/src/api/user/dFriends.js b/src/api/user/dFriends.js index 803dd89..11d4423 100644 --- a/src/api/user/dFriends.js +++ b/src/api/user/dFriends.js @@ -1,15 +1,12 @@ export async function dFriends(request, reply, fastify, getUserInfo, deleteFriends) { try { - if (!request.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } 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" }); - } deleteFriends.run(userId); return reply.code(200).send({ msg: "Friends deleted successfully" }); } catch (err) { diff --git a/src/api/user/dMatchHistory.js b/src/api/user/dMatchHistory.js index 8b7cc0e..241813b 100644 --- a/src/api/user/dMatchHistory.js +++ b/src/api/user/dMatchHistory.js @@ -1,15 +1,12 @@ export async function dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris) { try { - if (!request.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } 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 { game } = request.query; if (game !== 'pong' && game !== 'tetris') { return reply.code(400).send({ error: "Specified game does not exist" }); diff --git a/src/api/user/dMember.js b/src/api/user/dMember.js index 240a297..32630b9 100644 --- a/src/api/user/dMember.js +++ b/src/api/user/dMember.js @@ -1,22 +1,19 @@ export async function dMember(request, reply, fastify, getUserInfo, changeDisplayName) { try { - if (!request.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } 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 (user === 'admin' || user === request.params.userId) { - if (member === 'displayName') { - changeDisplayName.run("", request.params.userId); - return reply.code(200).send({ msg: "Display name deleted successfully" }); - } - return reply.code(400).send({ msg: "Member does not exist" }) + if (member === 'displayName') { + changeDisplayName.run("", request.params.userId); + return reply.code(200).send({ msg: "Display name deleted successfully" }); } else { - return reply.code(401).send({ error: 'You dont have the right to delete this' }); + return reply.code(400).send({ msg: "Member does not exist" }) } } catch (err) { fastify.log.error(err); diff --git a/src/api/user/dUser.js b/src/api/user/dUser.js index d2728af..864d8cb 100644 --- a/src/api/user/dUser.js +++ b/src/api/user/dUser.js @@ -1,6 +1,10 @@ export async function dUser(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteFriends, deleteUser) { try { - if (!getUserInfo.get(request.params.userId)) { + 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" }); } deleteMatchHistory.run('pong', request.params.userId); diff --git a/src/api/user/default.js b/src/api/user/default.js index 79b5786..2a3ee89 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -173,6 +173,7 @@ export default async function(fastify, options) { if (jwt.user !== 'admin') { throw ('You lack administrator privileges'); } + request.user = jwt.user; } catch (err) { reply.code(401).send({ error: 'Unauthorized' }); } diff --git a/src/api/user/gAvatar.js b/src/api/user/gAvatar.js index 6c0d3f6..f7c386f 100644 --- a/src/api/user/gAvatar.js +++ b/src/api/user/gAvatar.js @@ -1,6 +1,9 @@ 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" }); } diff --git a/src/api/user/gFriends.js b/src/api/user/gFriends.js index 5fbb941..8050e17 100644 --- a/src/api/user/gFriends.js +++ b/src/api/user/gFriends.js @@ -1,6 +1,9 @@ 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" }); } diff --git a/src/api/user/gMatchHistory.js b/src/api/user/gMatchHistory.js index 6999c9b..e8d3a55 100644 --- a/src/api/user/gMatchHistory.js +++ b/src/api/user/gMatchHistory.js @@ -1,6 +1,9 @@ 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" }); } diff --git a/src/api/user/gNumberFriends.js b/src/api/user/gNumberFriends.js index ca1ec24..03730bf 100644 --- a/src/api/user/gNumberFriends.js +++ b/src/api/user/gNumberFriends.js @@ -1,9 +1,13 @@ 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) { diff --git a/src/api/user/gNumberMatches.js b/src/api/user/gNumberMatches.js index f26e628..a784fac 100644 --- a/src/api/user/gNumberMatches.js +++ b/src/api/user/gNumberMatches.js @@ -1,6 +1,9 @@ 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" }); } diff --git a/src/api/user/gUser.js b/src/api/user/gUser.js index 6937e6c..24648bd 100644 --- a/src/api/user/gUser.js +++ b/src/api/user/gUser.js @@ -1,11 +1,24 @@ export async function gUser(request, reply, fastify, getUserInfo) { try { const userId = request.params.userId; - const userInfo = getUserInfo.get(userId); - if (!userInfo) { + 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" }); } - 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 } }); + return reply.code(200).send({ + username: userInfo.username, + displayName: userInfo.displayName, + pong: { + wins: userInfo.pongWins, + losses: userInfo.pongLosses + }, + tetris: { + wins: userInfo.tetrisWins, + losses: userInfo.tetrisLosses + } + }); } catch (err) { fastify.log.error(err); return reply.code(500).send({ error: "Internal server error" }); diff --git a/src/api/user/pAvatar.js b/src/api/user/pAvatar.js index a567589..f90299e 100644 --- a/src/api/user/pAvatar.js +++ b/src/api/user/pAvatar.js @@ -8,6 +8,9 @@ import sharp from 'sharp'; 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" }); } diff --git a/src/api/user/pFriend.js b/src/api/user/pFriend.js index dfb5341..ae7d414 100644 --- a/src/api/user/pFriend.js +++ b/src/api/user/pFriend.js @@ -1,11 +1,8 @@ export async function pFriend(request, reply, fastify, getUserInfo, getFriend, addFriend) { try { const userId = request.params.userId; - if (!request.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - if (request.user !== 'admin' && request.user !== userId) { - return reply.code(401).send({ error: "Unauthorized" }); + 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" }); diff --git a/src/api/user/pMatchHistory.js b/src/api/user/pMatchHistory.js index 584b53e..f722775 100644 --- a/src/api/user/pMatchHistory.js +++ b/src/api/user/pMatchHistory.js @@ -16,11 +16,11 @@ async function fetchSave(request, reply, userId, addMatch) { export async function pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris) { try { const userId = request.params.userId; - if (!request.user) { - return reply.code(400).send({ error: "Please specify a user" }); + if (request.user !== userId && request.user !== 'admin') { + return reply.code(401).send({ error: 'Unauthorized' }); } - 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" }); } if (request.body.game !== 'pong' && request.body.game !== 'tetris') { return reply.code(400).send({ error: "Specified game does not exist" }); diff --git a/src/api/user/pUser.js b/src/api/user/pUser.js index c5dd88e..31ce8bd 100644 --- a/src/api/user/pUser.js +++ b/src/api/user/pUser.js @@ -1,10 +1,7 @@ export async function pUser(request, reply, fastify, getUserInfo, createUser) { try { const userId = request.params.userId; - if (!request.user || !request.user.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - if (request.user.user !== 'admin') { + if (request.user !== 'admin') { return reply.code(401).send({ error: "Unauthorized" }); } if (getUserInfo.get(userId)) { diff --git a/src/api/user/uAvatar.js b/src/api/user/uAvatar.js index 1076e44..344307e 100644 --- a/src/api/user/uAvatar.js +++ b/src/api/user/uAvatar.js @@ -3,8 +3,11 @@ 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.cose(404).send({ error: "User does not exist" }); + return reply.code(404).send({ error: "User does not exist" }); } deleteAvatarId.run(userId); const parts = request.parts(); diff --git a/src/api/user/uMember.js b/src/api/user/uMember.js index 01c1dc5..adf5fce 100644 --- a/src/api/user/uMember.js +++ b/src/api/user/uMember.js @@ -1,11 +1,8 @@ export async function uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId) { try { const userId = request.params.userId; - if (!request.user) { - return reply.code(400).send({ error: "Please specify a user" }); - } - if (request.user !== 'admin' && request.user !== userId) { - return reply.code(401).send({ error: "Unauthorized" }); + 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" }); From 3c1c22a441155c84b68308cf04b098dfc39a0353 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 15:54:10 +0200 Subject: [PATCH 35/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix(cors?= =?UTF-8?q?):=20fixed=20some=20things.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth/default.js | 7 +++++++ src/api/user/default.js | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/api/auth/default.js b/src/api/auth/default.js index 422d794..5b80ed7 100644 --- a/src/api/auth/default.js +++ b/src/api/auth/default.js @@ -1,5 +1,6 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; +import cors from '@fastify/cors'; import { register } from './register.js'; import { login } from './login.js'; @@ -24,6 +25,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: { diff --git a/src/api/user/default.js b/src/api/user/default.js index 2a3ee89..8072895 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -1,6 +1,7 @@ import fastifyJWT from '@fastify/jwt'; import fastifyCookie from '@fastify/cookie'; import Database from 'better-sqlite3'; +import cors from '@fastify/cors'; import { gUsers } from './gUsers.js'; import { gUser } from './gUser.js'; @@ -150,6 +151,11 @@ export default async function(fastify, options) { }, }); fastify.register(fastifyCookie); + fastify.register(cors, { + origin: process.env.CORS_ORIGIN || 'http://localhost:5173', + credentials: true, + methods: [ "GET", "POST", "PATCH", "DELETE", "OPTIONS" ] + }); fastify.addContentTypeParser( ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], From e15cb8e776b7d51b89d6166446717087ce884615 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 16:00:24 +0200 Subject: [PATCH 36/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix(auth?= =?UTF-8?q?):=20remove=20working=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/user/dUser.js | 12 ++++++------ src/api/user/gUser.js | 1 + src/utils/authDB.js | 2 +- src/utils/authUserRemove.js | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/api/user/dUser.js b/src/api/user/dUser.js index 864d8cb..fad5571 100644 --- a/src/api/user/dUser.js +++ b/src/api/user/dUser.js @@ -1,16 +1,16 @@ export async function dUser(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteFriends, deleteUser) { try { - const userId = request.params.userId; - if (request.user !== userId && request.user !== 'admin') { + 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', request.params.userId); - deleteMatchHistory.run('tetris', request.params.userId); - deleteFriends.run(request.params.userId); - deleteUser.run(request.params.userId); + 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); diff --git a/src/api/user/gUser.js b/src/api/user/gUser.js index 24648bd..6750f75 100644 --- a/src/api/user/gUser.js +++ b/src/api/user/gUser.js @@ -7,6 +7,7 @@ export async function gUser(request, reply, fastify, getUserInfo) { 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, diff --git a/src/utils/authDB.js b/src/utils/authDB.js index 69d1bde..a4611f3 100644 --- a/src/utils/authDB.js +++ b/src/utils/authDB.js @@ -83,7 +83,7 @@ function getUser(user) { } function rmUser(user) { - const stmt = database.prepare('DELETE * FROM credentials WHERE username = ?'); + const stmt = database.prepare('DELETE FROM credentials WHERE username = ?'); stmt.run(user); } diff --git a/src/utils/authUserRemove.js b/src/utils/authUserRemove.js index 8af7503..2fc3bfc 100644 --- a/src/utils/authUserRemove.js +++ b/src/utils/authUserRemove.js @@ -8,7 +8,7 @@ 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.post( + await axios.delete( url, { headers: { From 583cf55cf81369812ed11712dab8532b8e7e53cd Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 16:01:37 +0200 Subject: [PATCH 37/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix(auth?= =?UTF-8?q?):=20now=20logout=20on=20removal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth/remove.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/auth/remove.js b/src/api/auth/remove.js index 3f653c6..ea1cbce 100644 --- a/src/api/auth/remove.js +++ b/src/api/auth/remove.js @@ -24,6 +24,7 @@ export async function remove(request, reply, fastify) { return reply .code(200) + .clearCookie("token") .send({ msg: "User successfully deleted" }) From f628b038d011a2929278a2b489b826d3df9afd75 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 16:05:31 +0200 Subject: [PATCH 38/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix(fron?= =?UTF-8?q?t):=20added=20exception=20catching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/user/default.js | 4 +-- src/front/static/ts/views/ProfileMenu.ts | 6 ++--- src/front/static/ts/views/RegisterPage.ts | 31 +++++++++++------------ 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/api/user/default.js b/src/api/user/default.js index 1c63b59..2c7902b 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -59,9 +59,9 @@ const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;' export default async function(fastify, options) { fastify.register(cors, { - origin: process.ENV.CORS_ORIGIN || 'http://localhost:5173', + origin: process.env.CORS_ORIGIN || 'http://localhost:5173', credentials: true, - methods: [ "GET", "POST", "DELETE", "OPTIONS" ] + methods: [ "GET", "POST", "PATCH", "DELETE", "OPTIONS" ] }); fastify.register(fastifyJWT, { diff --git a/src/front/static/ts/views/ProfileMenu.ts b/src/front/static/ts/views/ProfileMenu.ts index 40d68f6..84422ca 100644 --- a/src/front/static/ts/views/ProfileMenu.ts +++ b/src/front/static/ts/views/ProfileMenu.ts @@ -40,13 +40,13 @@ export default class extends Aview { async function getMainHTML() { if (!(await isLogged())) { - document.getElementById("menu-bottom-div").classList.add("hidden"); + document.getElementById("menu-bottom-div")?.classList.add("hidden"); return ` login register `; } - document.getElementById("menu-bottom-div").classList.remove("hidden"); + document.getElementById("menu-bottom-div")?.classList.remove("hidden"); uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, { @@ -71,7 +71,7 @@ export default class extends Aview { document.getElementById("profile-items").innerHTML = await getMainHTML(); - document.getElementById("menu-logout").addEventListener("click", async () => { + document.getElementById("menu-logout")?.addEventListener("click", async () => { let req = await fetch("http://localhost:3001/logout", { method: "GET", credentials: "include", diff --git a/src/front/static/ts/views/RegisterPage.ts b/src/front/static/ts/views/RegisterPage.ts index 30618f1..7485e93 100644 --- a/src/front/static/ts/views/RegisterPage.ts +++ b/src/front/static/ts/views/RegisterPage.ts @@ -5,8 +5,7 @@ import { isLogged, navigationManager } from "../main.ts" export default class extends Aview { - constructor() - { + constructor() { super(); this.setTitle("register"); setOnekoState("default"); @@ -53,7 +52,7 @@ export default class extends Aview { } async run() { - dragElement(document.getElementById("window")); + dragElement(document.getElementById("window")); const login = async () => { const username = (document.getElementById("username") as HTMLInputElement).value; const password = (document.getElementById("password") as HTMLInputElement).value; @@ -67,34 +66,34 @@ export default class extends Aview { }); const data = await data_req.json(); - if (data_req.status === 200) - { + if (data_req.status === 200) { let uuid_req = await fetch("http://localhost:3001/me", { method: "GET", credentials: "include", }); let uuid = await uuid_req.json(); - document.cookie = `uuid=${uuid.user};max-ages=${60*60*24*7}`; + document.cookie = `uuid=${uuid.user};max-ages=${60 * 60 * 24 * 7}`; console.log(document.cookie); isLogged(); navigationManager("/"); } - else if (data_req.status === 400) - { - document.getElementById("login-error-message").innerHTML = "error: " + data.error; - document.getElementById("login-error-message").classList.remove("hidden"); + 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 - { + else { throw new Error("invalid response"); } } - catch (error) - { + catch (error) { console.error(error); - document.getElementById("login-error-message").innerHTML = "error: server error, try again later..."; - document.getElementById("login-error-message").classList.remove("hidden"); + 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"); + } } }; From 9ea154492c7fea56428b67f87fccf4de50ff38d4 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 17:35:38 +0200 Subject: [PATCH 39/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20f?= =?UTF-8?q?ixed=20some=20things.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/user/gUser.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/api/user/gUser.js b/src/api/user/gUser.js index 6750f75..78e9a71 100644 --- a/src/api/user/gUser.js +++ b/src/api/user/gUser.js @@ -1,9 +1,6 @@ export async function gUser(request, reply, fastify, getUserInfo) { 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" }); } From d0862a50985c1f172a646e9b9f9f9b28d63e02ae Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 17:56:28 +0200 Subject: [PATCH 40/45] =?UTF-8?q?=E3=80=8C=E2=9C=A8=E3=80=8D=20feat:=20add?= =?UTF-8?q?ed=202fa=20checker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth/default.js | 8 ++++++-- src/api/auth/totpCheck.js | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/api/auth/totpCheck.js diff --git a/src/api/auth/default.js b/src/api/auth/default.js index 5b80ed7..ecba601 100644 --- a/src/api/auth/default.js +++ b/src/api/auth/default.js @@ -2,6 +2,7 @@ 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'; @@ -28,7 +29,7 @@ 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" ] + methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"] }); fastify.register(fastifyJWT, { @@ -54,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) => { @@ -119,5 +123,5 @@ export default async function(fastify, options) { fastify.get('/logout', {}, async (request, reply) => { return logout(reply, fastify); }) - fastify.delete('/', { preHandler: fastify.authenticate }, async (request, reply) => { return remove(request, reply, fastify)}) + fastify.delete('/', { preHandler: fastify.authenticate }, async (request, reply) => { return remove(request, reply, fastify) }) } diff --git a/src/api/auth/totpCheck.js b/src/api/auth/totpCheck.js new file mode 100644 index 0000000..d76a12e --- /dev/null +++ b/src/api/auth/totpCheck.js @@ -0,0 +1,24 @@ +import authDB from '../../utils/authDB.js'; + +/** + * @param {import('fastify').FastifyRequest} request + * @param {import('fastify').FastifyReply} reply + */ +export async function totpCheck(request, reply) { + try { + const user = request.user; + + if (authDB.checkUser(user) === false) { + return reply.code(400).send({ error: "User does not exist" }); + } + + return reply + .code(200) + .send({ + totp: authDB.isTOTPEnabled(user) + }); + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} From 4918618aad3e430539a6801d78d7094b7ebb723c Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 19:18:09 +0200 Subject: [PATCH 41/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20f?= =?UTF-8?q?ixed=20some=20things.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/user/default.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/api/user/default.js b/src/api/user/default.js index 8072895..13d81b3 100644 --- a/src/api/user/default.js +++ b/src/api/user/default.js @@ -143,6 +143,10 @@ const querySchemaMatchHistory = { type: 'object', required: ['game', 'iStart', ' 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 + */ export default async function(fastify, options) { fastify.register(fastifyJWT, { secret: process.env.JWT_SECRET || '123456789101112131415161718192021', @@ -163,7 +167,6 @@ export default async function(fastify, options) { async (request, payload) => payload ); - fastify.decorate('authenticate', async function(request, reply) { try { const jwt = await request.jwtVerify(); @@ -224,7 +227,7 @@ export default async function(fastify, options) { fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMatchHistory } }, async (request, reply) => { return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris); }); - fastify.post('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => { + 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) => { From b290915249ca5257ab6103bd28baab43e70d39c6 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 19:31:55 +0200 Subject: [PATCH 42/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20f?= =?UTF-8?q?ixed=20some=20things.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/static/ts/views/LoginPage.ts | 16 ++- src/front/static/ts/views/Settings.ts | 145 ++++++++++++++++-------- src/front/static/ts/views/TotpEnable.ts | 110 ++++++++++++++++++ 3 files changed, 217 insertions(+), 54 deletions(-) create mode 100644 src/front/static/ts/views/TotpEnable.ts diff --git a/src/front/static/ts/views/LoginPage.ts b/src/front/static/ts/views/LoginPage.ts index da0deec..97e6ed0 100644 --- a/src/front/static/ts/views/LoginPage.ts +++ b/src/front/static/ts/views/LoginPage.ts @@ -73,11 +73,17 @@ export default class extends Aview { } else if (data_req.status === 401) { const data = await data_req.json(); - const error = document.createElement("p"); - error.innerHTML = data.error; - error.classList.add("text-red-700", "dark:text-red-500"); + 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; + } - idWindow.appendChild(error); } else { console.log(data_req.status) console.log(await data_req.json()) @@ -140,7 +146,7 @@ export default class extends Aview { 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"); + 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"; diff --git a/src/front/static/ts/views/Settings.ts b/src/front/static/ts/views/Settings.ts index 94e6500..021b215 100644 --- a/src/front/static/ts/views/Settings.ts +++ b/src/front/static/ts/views/Settings.ts @@ -2,12 +2,12 @@ import Aview from "./Aview.ts" import { dragElement } from "./drag.ts"; import { setOnekoState } from "../oneko.ts" import { isLogged, navigationManager } from "../main.ts" - +import { totpEnablePopup } from "./TotpEnable.ts"; +import { totpVerify } from "../../../../api/auth/totpVerify.js"; export default class extends Aview { - constructor() - { + constructor() { super(); this.setTitle("profile"); setOnekoState("default"); @@ -29,62 +29,109 @@ export default class extends Aview {
- + `; } - async run() { - if (!await isLogged()) - navigationManager("/"); + async run() { + if (!await isLogged()) + navigationManager("/"); - dragElement(document.getElementById("window")); + dragElement(document.getElementById("window")); - let uuid: String; - uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; - const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, { - method: "GET", - credentials: "include", - }); - if (userdata_req.status == 404) { - console.error("invalid user"); - return; - } - let userdata = await userdata_req.json(); + const isTOTPEnabled = async () => { + const totpVerify_req = await fetch('http://localhost:3001/2fa', { + method: "GET", + credentials: "include" + }) - (document.getElementById("displayName-input") as HTMLInputElement).placeholder = userdata.displayName; - (document.getElementById("displayName-input") as HTMLInputElement).value = userdata.displayName; + if (totpVerify_req.status === 200) { + const totpVerify_data = await totpVerify_req.json(); + if (totpVerify_data.totp == true) { + return true; + } + } + return false; + }; - document.getElementById("displayName-button")?.addEventListener("click", async () => { - const changeDisplayName_req = await fetch(`http://localhost:3002/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 - } - }); + let uuid: String; + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + const userdata_req = await fetch(`http://localhost:3002/users/${uuid}`, { + method: "GET", + credentials: "include", + }); + if (userdata_req.status == 404) { + console.error("invalid user"); + return; + } + let userdata = await userdata_req.json(); - document.getElementById("deleteAccount-button")?.addEventListener("click", async () => { - const delete_req = await fetch(`http://localhost:3001/`, { - method: "DELETE", - credentials: "include", - }); + (document.getElementById("displayName-input") as HTMLInputElement).placeholder = userdata.displayName; + (document.getElementById("displayName-input") as HTMLInputElement).value = userdata.displayName; - if (delete_req.status == 200) - navigationManager("/"); - else - console.error("xd"); // xd????????????? - }); + document.getElementById("displayName-button")?.addEventListener("click", async () => { + const changeDisplayName_req = await fetch(`http://localhost:3002/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("2fa-button")?.addEventListener("click", async () => { - - }); - } + document.getElementById("deleteAccount-button")?.addEventListener("click", async () => { + const delete_req = await fetch(`http://localhost:3001/`, { + method: "DELETE", + credentials: "include", + }); + + if (delete_req.status == 200) + navigationManager("/"); + else + console.error("xd"); // xd????????????? + }); + + + const totpButton = document.getElementById("2fa-button") as HTMLButtonElement; + + if ((await isTOTPEnabled()) === true) { + totpButton.innerHTML = "disable 2fa"; + + document.getElementById("2fa-button")?.addEventListener("click", async () => { + const totp_req = await fetch(`http://localhost:3001/2fa`, { + method: "DELETE", + credentials: "include" + }) + if (totp_req.status === 200) { + console.log("working") + navigationManager("/settings") + } else { + console.log("wut") + } + }); + } else { + totpButton.innerHTML = "enable 2fa"; + + document.getElementById("2fa-button")?.addEventListener("click", async () => { + const totp_req = await fetch(`http://localhost:3001/2fa`, { + method: "POST", + credentials: "include" + }) + if (totp_req.status === 200) { + console.log("working") + const totp_data = await totp_req.json(); + totpEnablePopup(uuid, totp_data.secret, totp_data.otpauthUrl); + } else { + console.log("wut") + } + }); + } + } } diff --git a/src/front/static/ts/views/TotpEnable.ts b/src/front/static/ts/views/TotpEnable.ts new file mode 100644 index 0000000..89488c1 --- /dev/null +++ b/src/front/static/ts/views/TotpEnable.ts @@ -0,0 +1,110 @@ +import { navigationManager } from "../main.ts"; +import { dragElement } from "./drag.ts"; + +async function totpVerify() { + const code = (document.getElementById("totpPin") as HTMLInputElement).value; + const data_req = await fetch('http://localhost:3001/2fa/verify', { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + token: code + }) + }) + + if (data_req.status === 200) { + navigationManager("/settings"); + } else if (data_req.status === 401 || data_req.status === 400) { + const popup_content = document.getElementById("2fa-enable-content"); + + if (!document.getElementById("error-totp")) { + const error = document.createElement("p"); + error.id = "error-totp"; + error.classList.add("text-red-700", "dark:text-red-500"); + error.innerHTML = (await data_req.json()).error; + + popup_content?.appendChild(error) + } else { + const error = document.getElementById("error-totp") as HTMLParagraphElement; + error.innerHTML = (await data_req.json()).error; + } + } else { + console.log("Unexpected error") + } +} + +export async function totpEnablePopup(username: String, secret: String, url: String) { + const popup: HTMLDivElement = document.createElement("div"); + popup.id = "2fa-enable-popup"; + popup.classList.add("z-10", "absolute", "default-border"); + const header = popup.appendChild(document.createElement("div"));; + header.classList.add("bg-linear-to-r", "from-orange-200", "to-orange-300", "flex", "flex-row", "min-w-35", "justify-between", "px-2"); + header.id = "2fa-enable-popup-header"; + header.appendChild(document.createElement("span")).innerText = "2fa_enable.ts"; + const btn = header.appendChild(document.createElement("button")); + btn.innerText = " × "; + btn.onclick = () => { document.getElementById("2fa-enable-popup")?.remove(); }; + + const popup_content: HTMLSpanElement = popup.appendChild(document.createElement("div")); + popup_content.id = "2fa-enable-content"; + popup_content.classList.add("flex", "flex-col", "bg-neutral-200", "dark:bg-neutral-800", "p-6", "pt-4", "text-neutral-900", "dark:text-white", "space-y-4"); + + const qrDivTOTP = document.createElement("div"); + qrDivTOTP.classList.add("flex", "justify-center"); + + const qrCodeTOTP = document.createElement("img"); + qrCodeTOTP.id = "qrCodeTOTP"; + qrCodeTOTP.src = `https://api.qrserver.com/v1/create-qr-code/?margin=10&size=512x512&data=${url}`; + qrCodeTOTP.classList.add("w-60"); + qrDivTOTP.appendChild(qrCodeTOTP); + + const secretText = document.createElement("p"); + secretText.innerHTML = `key:
${secret}
`; + secretText.classList.add("text-center") + + const tokenInput = document.createElement("input"); + tokenInput.type = "tel"; + tokenInput.id = "totpPin"; + tokenInput.name = "totpPin"; + tokenInput.placeholder = "TOTP code"; + tokenInput.required = true; + tokenInput.autocomplete = "off"; + tokenInput.pattern = "[0-9]*"; + tokenInput.setAttribute("inputmode", "numeric"); + tokenInput.classList.add("bg-white", "text-neutral-900", "w-full", "px-4", "py-2", "input-border"); + + const tokenSubmit = document.createElement("button"); + tokenSubmit.type = "submit"; + tokenSubmit.classList.add("default-button", "w-full"); + tokenSubmit.id = "totp-submit"; + tokenSubmit.innerHTML = "submit"; + + const hr = document.createElement("hr"); + hr.classList.add("my-2", "w-full", "reverse-border"); + + const t = document.createElement("h2"); + t.innerHTML = "hey " + username + + ` you are trying to add 2fa
+ just add the following to your app and enter the code bellow ↓ + `; + t.classList.add("text-center") + + document.getElementById("app")?.appendChild(popup); + + const form = document.createElement("form"); + form.method = "dialog"; + form.classList.add("space-y-4"); + form.appendChild(tokenInput); + form.appendChild(tokenSubmit); + + popup_content.appendChild(t) + popup_content.appendChild(qrDivTOTP); + popup_content.appendChild(secretText); + popup_content.appendChild(hr) + popup_content.appendChild(form); + dragElement(document.getElementById("2fa-enable-popup")); + + document.getElementById("totp-submit")?.addEventListener("click", totpVerify) +} From bfaed233651ca4c393b8b4c65321dbe95d89a971 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 19:41:35 +0200 Subject: [PATCH 43/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20c?= =?UTF-8?q?entered=20the=20error=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/static/ts/views/TotpEnable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/front/static/ts/views/TotpEnable.ts b/src/front/static/ts/views/TotpEnable.ts index 89488c1..baf4fd9 100644 --- a/src/front/static/ts/views/TotpEnable.ts +++ b/src/front/static/ts/views/TotpEnable.ts @@ -22,7 +22,7 @@ async function totpVerify() { if (!document.getElementById("error-totp")) { const error = document.createElement("p"); error.id = "error-totp"; - error.classList.add("text-red-700", "dark:text-red-500"); + error.classList.add("text-red-700", "dark:text-red-500", "text-center"); error.innerHTML = (await data_req.json()).error; popup_content?.appendChild(error) From 774581b9f64dd69e7cbd4674fc70323c6fe55609 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 20:08:09 +0200 Subject: [PATCH 44/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20f?= =?UTF-8?q?ixed=20the=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/static/ts/views/Settings.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/front/static/ts/views/Settings.ts b/src/front/static/ts/views/Settings.ts index 87e3066..3019af6 100644 --- a/src/front/static/ts/views/Settings.ts +++ b/src/front/static/ts/views/Settings.ts @@ -24,7 +24,7 @@ export default class extends Aview { × -
+
@@ -37,7 +37,9 @@ export default class extends Aview {
-
+
+
+
From b5d2244035a114c9085dbba6089de010dac7bbe5 Mon Sep 17 00:00:00 2001 From: adjoly Date: Thu, 23 Oct 2025 20:09:33 +0200 Subject: [PATCH 45/45] =?UTF-8?q?=E3=80=8C=F0=9F=94=A8=E3=80=8D=20fix:=20f?= =?UTF-8?q?ixed=20the=20profile=20page=20with=20the=20shitty=20profile=20p?= =?UTF-8?q?icture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/static/ts/views/Profile.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/front/static/ts/views/Profile.ts b/src/front/static/ts/views/Profile.ts index 4374d70..6dce472 100644 --- a/src/front/static/ts/views/Profile.ts +++ b/src/front/static/ts/views/Profile.ts @@ -188,7 +188,9 @@ export default class extends Aview { method: "GET", credentials: "include", }); - picture.src = a.status === 200 ? `http://localhost:3002/users/${uuid}/avatar` : "https://api.kanel.ovh/pp"; + picture.src = a.status === 200 + ? `http://localhost:3002/users/${uuid}/avatar?t=${Date.now()}` + : "https://api.kanel.ovh/pp"; picture.classList.add("text-neutral-900", "dark:text-white", "center", "h-18", "w-18", "mx-3", "reverse-border"); const nametag = profile.appendChild(document.createElement("div"));