mirror of
https://github.com/KeyZox71/knl_meowscendence.git
synced 2025-10-14 02:54:44 +02:00
WIP API, match history done
This commit is contained in:
39
src/api/user/TODO
Normal file
39
src/api/user/TODO
Normal file
@ -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 ?
|
@ -1,23 +1,25 @@
|
|||||||
import fastifyJWT from '@fastify/jwt';
|
import fastifyJWT from '@fastify/jwt';
|
||||||
import fastifyCookie from '@fastify/cookie';
|
import fastifyCookie from '@fastify/cookie';
|
||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
var env = process.env.NODE_ENV || 'development';
|
const env = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
let database;
|
|
||||||
|
|
||||||
if (!env || env === 'development') {
|
if (!env || env === 'development') {
|
||||||
database = new Database(":memory:", { verbose: console.log });
|
const database = new Database(":memory:", { verbose: console.log });
|
||||||
} else {
|
} else {
|
||||||
var dbPath = process.env.DB_PATH || '/db/db.sqlite'
|
const dbPath = process.env.DB_PATH || '/db/db.sqlite'
|
||||||
database = new Database(dbPath);
|
const database = new Database(dbPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareDB() {
|
function prepareDB() {
|
||||||
database.exec(`
|
database.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS userData (
|
CREATE TABLE IF NOT EXISTS userData (
|
||||||
username TEXT PRIMARY KEY,
|
username TEXT PRIMARY KEY,
|
||||||
displayName TEXT
|
displayName TEXT,
|
||||||
|
avatar BLOB,
|
||||||
|
wins INTEGER,
|
||||||
|
losses INTEGER
|
||||||
) STRICT
|
) STRICT
|
||||||
`);
|
`);
|
||||||
database.exec(`
|
database.exec(`
|
||||||
@ -26,30 +28,39 @@ function prepareDB() {
|
|||||||
friendName TEXT,
|
friendName TEXT,
|
||||||
UNIQUE(username, friendName),
|
UNIQUE(username, friendName),
|
||||||
CHECK(username != friendName)
|
CHECK(username != friendName)
|
||||||
)
|
) STRICT
|
||||||
|
`);
|
||||||
|
database.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS matchHistory (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT,
|
||||||
|
matchId INTEGER
|
||||||
|
) STRICT
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareDB();
|
prepareDB();
|
||||||
|
|
||||||
// POST
|
// 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 addFriend = database.prepare('INSERT INTO friends (username, friendName) VALUES (?, ?);');
|
||||||
|
const addMatch = database.prepare('INSERT INTO matchHistory (username, matchId) VALUES (?, ?);');
|
||||||
|
|
||||||
// PATCH
|
// PATCH
|
||||||
const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;');
|
const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;');
|
||||||
|
const changeAvatar = database.prepare('UPDATE userData SET avatar = ? WHERE username = ?;');
|
||||||
|
|
||||||
// GET
|
// GET
|
||||||
const getUserInfo = database.prepare('SELECT * FROM userData WHERE username = ?;');
|
|
||||||
const getUserData = database.prepare('SELECT * FROM userData;');
|
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 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
|
// DELETE
|
||||||
const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;');
|
const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;');
|
||||||
const deleteFriend = database.prepare('DELETE FROM friends WHERE username = ? AND friendName = ?;');
|
const deleteFriend = database.prepare('DELETE FROM friends WHERE username = ? AND friendName = ?;');
|
||||||
const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;');
|
const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;');
|
||||||
|
const deleteMatchHistory = database.prepare('DELETE FROM matchHistory WHERE username = ?;');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('fastify').FastifyInstance} fastify
|
* @param {import('fastify').FastifyInstance} fastify
|
||||||
@ -88,7 +99,6 @@ export default async function(fastify, options) {
|
|||||||
fastify.get('/users', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
fastify.get('/users', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const users = getUserData.all();
|
const users = getUserData.all();
|
||||||
|
|
||||||
return reply.code(200).send({ users });
|
return reply.code(200).send({ users });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fastify.log.error(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) => {
|
fastify.get('/users/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const info = getUserInfo.get(request.params.userId);
|
const info = getUserInfo.get(request.params.userId);
|
||||||
|
|
||||||
return reply.code(200).send({ info });
|
return reply.code(200).send({ info });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fastify.log.error(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) => {
|
fastify.get('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const userId = request.params.userId;
|
const userId = request.params.userId;
|
||||||
|
|
||||||
if (!getUserInfo.get(userId)) {
|
if (!getUserInfo.get(userId)) {
|
||||||
return reply.code(404).send({ error: "User does not exist" });
|
return reply.code(404).send({ error: "User does not exist" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId == request.user || request.user == 'admin') {
|
if (userId == request.user || request.user == 'admin') {
|
||||||
const friends = getFriends.all(userId);
|
const friends = getFriends.all(userId);
|
||||||
|
|
||||||
if (!friends) {
|
if (!friends) {
|
||||||
return reply.code(404).send({ error: "User does not have friends D:" });
|
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" });
|
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
|
// POST
|
||||||
fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => {
|
fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const userId = request.params.userId;
|
const userId = request.params.userId;
|
||||||
|
if (request.user != 'admin') {
|
||||||
|
return reply.code(401).send({ error: "Unauthorized" });
|
||||||
|
}
|
||||||
if (getUserInfo.get(userId)) {
|
if (getUserInfo.get(userId)) {
|
||||||
return reply.code(400).send({ error: "User already exist" });
|
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" });
|
return reply.code(200).send({ msg: "User created sucessfully" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fastify.log.error(err);
|
fastify.log.error(err);
|
||||||
return reply.code(500).send({ error: "Internal server error" });
|
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', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const userId = request.params.userId;
|
const userId = request.params.userId;
|
||||||
@ -167,6 +215,37 @@ export default async function(fastify, options) {
|
|||||||
return reply.code(500).send({ error: "Internal server error" });
|
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
|
// PATCH
|
||||||
fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
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" });
|
return reply.code(404).send({ error: "User does not exist" });
|
||||||
}
|
}
|
||||||
const member = request.params.member;
|
const member = request.params.member;
|
||||||
|
|
||||||
if (member === 'displayName') {
|
if (member === 'displayName') {
|
||||||
if (!request.body || !request.body.displayName) {
|
if (!request.body || !request.body.displayName) {
|
||||||
return reply.code(400).send({ error: "Please specify a displayName" });
|
return reply.code(400).send({ error: "Please specify a displayName" });
|
||||||
}
|
}
|
||||||
|
|
||||||
changeDisplayName.run(request.body.displayName, userId);
|
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" })
|
return reply.code(400).send({ error: "Member does not exist" })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fastify.log.error(err);
|
fastify.log.error(err);
|
||||||
return reply.code(500).send({ error: "Internal server error" });
|
return reply.code(500).send({ error: "Internal server error" });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// DELETE
|
// DELETE
|
||||||
/**
|
/**
|
||||||
@ -218,9 +304,9 @@ export default async function(fastify, options) {
|
|||||||
if (user == 'admin' || user == request.params.userId) {
|
if (user == 'admin' || user == request.params.userId) {
|
||||||
if (member == 'displayName') {
|
if (member == 'displayName') {
|
||||||
changeDisplayName.run("", request.params.userId);
|
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 {
|
} else {
|
||||||
return reply.code(401).send({ error: 'You dont have the right to delete this' });
|
return reply.code(401).send({ error: 'You dont have the right to delete this' });
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user