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

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

View File

@ -1,7 +1,8 @@
import fastifyJWT from '@fastify/jwt';
import fastifyCookie from '@fastify/cookie';
import cors from '@fastify/cors'
import cors from '@fastify/cors';
import { totpCheck } from './totpCheck.js';
import { register } from './register.js';
import { login } from './login.js';
import { gRedir } from './gRedir.js';
@ -12,6 +13,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';
@ -25,9 +27,9 @@ authDB.prepareDB();
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, {
@ -53,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) => {
@ -117,4 +122,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) })
}

View File

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

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

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

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

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

View File

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

View File

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

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

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

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

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

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

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

View File

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

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

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

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

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

View File

@ -2,66 +2,158 @@ import fastifyJWT from '@fastify/jwt';
import fastifyCookie from '@fastify/cookie';
import cors from '@fastify/cors'
import Database from 'better-sqlite3';
import cors from '@fastify/cors';
var env = process.env.NODE_ENV || 'development';
import { gUsers } from './gUsers.js';
import { gUser } from './gUser.js';
import { gNumberUsers } from './gNumberUsers.js';
import { gFriends } from './gFriends.js';
import { gNumberFriends } from './gNumberFriends.js';
import { gMatchHistory } from './gMatchHistory.js';
import { gNumberMatches } from './gNumberMatches.js';
import { pUser } from './pUser.js';
import { pFriend } from './pFriend.js';
import { pMatchHistory } from './pMatchHistory.js';
import { uMember } from './uMember.js';
import { dUser } from './dUser.js';
import { dMember } from './dMember.js';
import { dFriends } from './dFriends.js';
import { dFriend } from './dFriend.js';
import { dMatchHistory } from './dMatchHistory.js';
import { pAvatar } from './pAvatar.js';
import { gAvatar } from './gAvatar.js';
import { uAvatar } from './uAvatar.js';
import { dAvatar } from './dAvatar.js';
import { pPing } from './pPing.js';
import { gPing } from './gPing.js';
const env = process.env.NODE_ENV || 'development';
let database;
if (!env || env === 'development') {
database = new Database(":memory:", { verbose: console.log });
database = new Database(':memory:', { verbose: console.log });
} else {
var dbPath = process.env.DB_PATH || '/db/db.sqlite'
const dbPath = process.env.DB_PATH || '/db/db.sqlite'
database = new Database(dbPath);
}
function prepareDB() {
database.exec(`
CREATE TABLE IF NOT EXISTS userData (
username TEXT PRIMARY KEY,
displayName TEXT
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
displayName TEXT,
avatarId INTEGER,
pongWins INTEGER,
pongLosses INTEGER,
tetrisWins INTEGER,
tetrisLosses INTEGER,
UNIQUE(username),
CHECK(pongWins >= 0),
CHECK(pongLosses >= 0),
CHECK(tetrisWins >= 0),
CHECK(tetrisLosses >= 0)
) STRICT
`);
database.exec(`
CREATE TABLE IF NOT EXISTS friends (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
friendName TEXT,
UNIQUE(username, friendName),
CHECK(username != friendName)
)
) STRICT
`);
database.exec(`
CREATE TABLE IF NOT EXISTS matchHistory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
game TEXT,
date INTEGER,
player1 TEXT,
player2 TEXT,
matchId INTEGER,
CHECK(game = 'pong' OR game = 'tetris'),
CHECK(date >= 0),
CHECK(player1 != player2)
) STRICT
`);
database.exec(`
CREATE TABLE IF NOT EXISTS activityTime (
username TEXT PRIMARY KEY,
time TEXT
) STRICT
`);
database.exec(`
CREATE TABLE IF NOT EXISTS images (
imageId INTEGER PRIMARY KEY AUTOINCREMENT,
fileName TEXT,
mimeType TEXT,
data BLOB
) STRICT
`);
}
prepareDB();
// POST
const createUser = database.prepare('INSERT INTO userData (username, displayName) VALUES (?, ?);');
const createUser = database.prepare('INSERT INTO userData (username, displayName, avatarId, pongWins, pongLosses, tetrisWins, tetrisLosses) VALUES (?, ?, -1, 0, 0, 0, 0);');
const addFriend = database.prepare('INSERT INTO friends (username, friendName) VALUES (?, ?);');
const addMatch = database.prepare('INSERT INTO matchHistory (game, date, player1, player2, matchId) VALUES (?, ?, ?, ?, ?);');
const incWinsPong = database.prepare('UPDATE userData SET pongWins = pongWins + 1 WHERE username = ?;');
const incLossesPong = database.prepare('UPDATE userData SET pongLosses = pongLosses + 1 WHERE username = ?');
const incWinsTetris = database.prepare('UPDATE userData SET tetrisWins = tetrisWins + 1 WHERE username = ?;');
const incLossesTetris = database.prepare('UPDATE userData SET tetrisLosses = tetrisLosses + 1 WHERE username = ?');
const setAvatarId = database.prepare('UPDATE userData SET avatarId = ? WHERE username = ?;');
const postImage = database.prepare('INSERT INTO images (fileName, mimeType, data) VALUES (?, ?, ?);');
const setActivityTime = database.prepare(`
INSERT INTO activityTime (username, time)
VALUES (?, ?)
ON CONFLICT(username) DO UPDATE SET time = excluded.time;
`);
// PATCH
const changeDisplayName = database.prepare('UPDATE userData SET displayName = ? WHERE username = ?;');
const changeAvatarId = database.prepare('UPDATE userData SET avatarId = ? WHERE username = ?;');
// GET
const getUserInfo = database.prepare('SELECT * FROM userData WHERE username = ?;');
const getUserData = database.prepare('SELECT * FROM userData;');
const getFriends = database.prepare('SELECT friendName FROM friends WHERE username = ?;');
// const isFriend = database.prepare('SELECT 1 FROM friends WHERE username = ? AND friendName = ?;');
const getUserData = database.prepare('SELECT username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses FROM userData LIMIT ? OFFSET ?;');
const getUserInfo = database.prepare('SELECT username, displayName, pongWins, pongLosses, tetrisWins, tetrisLosses FROM userData WHERE username = ?;');
const getFriends = database.prepare('SELECT friendName FROM friends WHERE username = ? LIMIT ? OFFSET ?;');
const getFriend = database.prepare('SELECT friendName FROM friends WHERE username = ? AND friendName = ?;');
const getMatchHistory = database.prepare('SELECT matchId, date FROM matchHistory WHERE game = ? AND ? IN (player1, player2) LIMIT ? OFFSET ?;');
const getNumberUsers = database.prepare('SELECT COUNT (DISTINCT username) AS n_users FROM userData;');
const getNumberFriends = database.prepare('SELECT COUNT (DISTINCT friendName) AS n_friends FROM friends WHERE username = ?;');
const getNumberMatches = database.prepare('SELECT COUNT (DISTINCT id) AS n_matches FROM matchHistory WHERE game = ? AND ? IN (player1, player2);');
const getAvatarId = database.prepare('SELECT avatarId FROM userData WHERE username = ?;');
const getImage = database.prepare('SELECT fileName, mimeType, data FROM images WHERE imageId = ?;');
const getActivityTime = database.prepare('SELECT time FROM activityTime WHERE username = ?;')
// DELETE
const deleteUser = database.prepare('DELETE FROM userData WHERE username = ?;');
const deleteFriend = database.prepare('DELETE FROM friends WHERE username = ? AND friendName = ?;');
const deleteFriends = database.prepare('DELETE FROM friends WHERE username = ?;');
const deleteMatchHistory = database.prepare('DELETE FROM matchHistory WHERE game = ? AND ? IN (player1, player2);');
const deleteStatsPong = database.prepare('UPDATE userData SET pongWins = 0, pongLosses = 0 WHERE username = ?;');
const deleteStatsTetris = database.prepare('UPDATE userData SET tetrisWins = 0, tetrisLosses = 0 WHERE username = ?;');
const deleteAvatarId = database.prepare('UPDATE userData SET avatarId = -1 WHERE username = ?;');
const deleteImage = database.prepare('DELETE FROM images WHERE imageId = ?;');
const querySchema = { type: 'object', required: ['iStart', 'iEnd'], properties: { iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } };
const bodySchemaMember = { type: 'object', properties: { displayName: { type: 'string' } } };
const querySchemaMatchHistory = { type: 'object', required: ['game', 'iStart', 'iEnd'], properties: { game: { type: 'string' }, iStart: { type: 'integer', minimum: 0 }, iEnd: { type: 'integer', minimum: 0 } } };
const bodySchemaMatchHistory = { type: 'object', required: ['game', 'date', 'myScore'], properties: { game: { type: 'string' }, date: { type: 'integer', minimum: 0 }, opponent: { type: 'string' }, myScore: { type: 'integer', minimum: 0 }, opponentScore: { type: 'integer', minimum: 0 } } };
const querySchemaMatchHistoryGame = { type: 'object', required: ['game'], properties: { game: { type: 'string' } } };
/**
* @param {import('fastify').FastifyInstance} fastify
* @param {import('fastify').FastifyPluginOptions} options
* @param {import('fastify').FastifyInstance} fastify
* @param {import('fastify').FastifyPluginOptions} options
*/
export default async function(fastify, options) {
fastify.register(cors, {
origin: process.ENV.CORS_ORIGIN || 'http://localhost:5173',
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
credentials: true,
methods: [ "GET", "POST", "DELETE", "OPTIONS" ]
methods: [ "GET", "POST", "PATCH", "DELETE", "OPTIONS" ]
});
fastify.register(fastifyJWT, {
@ -71,8 +163,19 @@ 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.decorate("authenticate", async function(request, reply) {
fastify.addContentTypeParser(
['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
{ parseAs: 'buffer' },
async (request, payload) => payload
);
fastify.decorate('authenticate', async function(request, reply) {
try {
const jwt = await request.jwtVerify();
request.user = jwt.user;
@ -81,178 +184,89 @@ export default async function(fastify, options) {
}
});
fastify.decorate("authenticateAdmin", async function(request, reply) {
fastify.decorate('authenticateAdmin', async function(request, reply) {
try {
const jwt = await request.jwtVerify();
if (jwt.user !== 'admin') {
throw ("");
throw ('You lack administrator privileges');
}
request.user = jwt.user;
} catch (err) {
reply.code(401).send({ error: 'Unauthorized' });
}
});
// GET
fastify.get('/users', { preHandler: [fastify.authenticate] }, async (request, reply) => {
try {
const users = getUserData.all();
return reply.code(200).send({ users });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
fastify.get('/users', { preHandler: [fastify.authenticate], schema: { querystring: querySchema } }, async (request, reply) => {
return gUsers(request, reply, fastify, getUserData);
});
fastify.get('/users/count', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return gNumberUsers(request, reply, fastify, getNumberUsers);
});
fastify.get('/users/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => {
try {
const info = getUserInfo.get(request.params.userId);
return reply.code(200).send({ info });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
return gUser(request, reply, fastify, getUserInfo);
});
fastify.get('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => {
try {
const userId = request.params.userId;
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
if (userId == request.user || request.user == 'admin') {
const friends = getFriends.all(userId);
if (!friends) {
return reply.code(404).send({ error: "User does not have friends D:" });
}
return reply.code(200).send({ friends });
}
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
fastify.get('/users/:userId/friends', { preHandler: [fastify.authenticate], schema: { querystring: querySchema } }, async (request, reply) => {
return gFriends(request, reply, fastify, getUserInfo, getFriends);
});
fastify.get('/users/:userId/friends/count', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return gNumberFriends(request, reply, fastify, getUserInfo, getNumberFriends);
});
fastify.get('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { querystring: querySchemaMatchHistory } }, async (request, reply) => {
return gMatchHistory(request, reply, fastify, getUserInfo, getMatchHistory);
});
fastify.get('/users/:userId/matchHistory/count', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => {
return gNumberMatches(request, reply, fastify, getUserInfo, getNumberMatches);
});
fastify.get('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return gAvatar(request, reply, fastify, getUserInfo, getAvatarId, getImage);
});
fastify.get('/ping/:userId', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return gPing(request, reply, fastify, getActivityTime);
});
// POST
fastify.post('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => {
try {
const userId = request.params.userId;
if (getUserInfo.get(userId)) {
return reply.code(400).send({ error: "User already exist" });
}
createUser.run(userId, userId);
return reply.code(200).send({ msg: "User created sucessfully" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
})
fastify.post('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => {
try {
const userId = request.params.userId;
if (request.user != 'admin' && request.user != userId) {
return reply.code(401).send({ error: "Unauthorized" });
}
if (!request.body || !request.body.user) {
return reply.code(400).send({ error: "Please specify a user" });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
if (!getUserInfo.get(request.body.user)) {
return reply.code(404).send({ error: "Friend does not exist" });
}
if (request.body.user === userId) {
return reply.code(400).send({ error: "You can't add yourself :D" });
}
addFriend.run(userId, request.body.user)
return reply.code(200).send({ msg: "Friend added sucessfully" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
return pUser(request, reply, fastify, getUserInfo, createUser);
});
fastify.post('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return pFriend(request, reply, fastify, getUserInfo, getFriend, addFriend);
});
fastify.post('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMatchHistory } }, async (request, reply) => {
return pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris);
});
fastify.post('/users/:userId/avatar', { bodyLimit: 5242880, preHandler: [fastify.authenticate] }, async (request, reply) => {
return pAvatar(request, reply, fastify, getUserInfo, setAvatarId, postImage);
});
fastify.post('/ping', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return pPing(request, reply, fastify, setActivityTime);
})
// PATCH
fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate] }, async (request, reply) => {
try {
const userId = request.params.userId;
if (request.user != 'admin' && request.user != userId) {
return reply.code(401).send({ error: "Unauthorized" });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
const member = request.params.member;
if (member === 'displayName') {
if (!request.body || !request.body.displayName) {
return reply.code(400).send({ error: "Please specify a displayName" });
}
changeDisplayName.run(request.body.displayName, userId);
return reply.code(200).send({ msg: "displayName modified sucessfully" });
}
return reply.code(400).send({ error: "Member does not exist" })
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
})
fastify.patch('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return uAvatar(request, reply, fastify, getUserInfo, setAvatarId, getAvatarId, deleteAvatarId, postImage, deleteImage);
});
fastify.patch('/users/:userId/:member', { preHandler: [fastify.authenticate], schema: { body: bodySchemaMember } }, async (request, reply) => {
return uMember(request, reply, fastify, getUserInfo, changeDisplayName, changeAvatarId);
});
// DELETE
/**
* @description Can be used to delete a user from the db
*/
fastify.delete('/users/:userId', { preHandler: [fastify.authenticateAdmin] }, async (request, reply) => {
try {
if (!getUserInfo(request.params.userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
deleteUser.run(request.params.userId);
deleteFriends.run(request.params.userId);
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
return dUser(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteFriends, deleteUser);
});
fastify.delete('/users/:userId/:member', { preHandler: fastify.authenticate }, async (request, reply) => {
try {
const user = request.user;
const member = request.params.member;
if (user == 'admin' || user == request.params.userId) {
if (member == 'displayName') {
changeDisplayName.run("", request.params.userId);
return reply.code(200).send({ msg: "displayName cleared sucessfully" });
}
return reply.code(400).send({ msg: "member does not exist" })
} else {
return reply.code(401).send({ error: 'You dont have the right to delete this' });
}
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
return dMember(request, reply, fastify, getUserInfo, changeDisplayName);
});
fastify.delete('/users/:userId/friends', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return dFriends(request, reply, fastify, getUserInfo, deleteFriends);
});
fastify.delete('/users/:userId/friends/:friendId', { preHandler: [fastify.authenticate] }, async (request, reply) => {
try {
const userId = request.params.userId;
const friendId = request.params.friendId;
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
if (request.user != 'admin' && request.user != userId) {
return reply.code(401).send({ error: "Unauthorized" });
}
deleteFriend.run(userId, friendId);
return reply.code(200).send({ msg: "Friend remove sucessfully" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
return dFriend(request, reply, fastify, getUserInfo, getFriend, deleteFriend);
});
fastify.delete('/users/:userId/matchHistory', { preHandler: [fastify.authenticate], schema: { query: querySchemaMatchHistoryGame } }, async (request, reply) => {
return dMatchHistory(request, reply, fastify, getUserInfo, deleteMatchHistory, deleteStatsPong, deleteStatsTetris);
});
fastify.delete('/users/:userId/avatar', { preHandler: [fastify.authenticate] }, async (request, reply) => {
return dAvatar(request, reply, fastify, getUserInfo, getAvatarId, deleteAvatarId, deleteImage);
});
}

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

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

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

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

View File

@ -0,0 +1,40 @@
export async function gMatchHistory(request, reply, fastify, getUserInfo, getMatchHistory) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
const { game, iStart, iEnd } = request.query;
if (game !== 'pong' && game !== 'tetris') {
return reply.code(400).send({ error: "Specified game does not exist" });
}
if (Number(iEnd) < Number(iStart)) {
return reply.code(400).send({ error: "Starting index cannot be strictly inferior to ending index" });
}
const matchHistoryId = getMatchHistory.all(game, userId, Number(iEnd) - Number(iStart), Number(iStart));
if (!matchHistoryId.length) {
return reply.code(404).send({ error: "No matches exist in the selected range" });
}
const promises = matchHistoryId.map(async (match) => {
const res = await fetch(`http://localhost:3003/${match.matchId}`, { method: "GET" });
if (!res.ok) {
throw new Error('Failed to fetch item from blockchain API');
}
const resJson = await res.json();
resJson.score.date = match.date;
if (resJson.score.p2 === "" && resJson.score.p2Score === 0) {
delete resJson.score.p2;
delete resJson.score.p2Score;
}
return resJson;
});
const matchHistory = await Promise.all(promises);
return reply.code(200).send({ matchHistory });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

View File

@ -0,0 +1,66 @@
async function fetchSave(request, reply, userId, addMatch) {
let opponentName = '';
let opponentScore = 0;
if (request.body.opponent && request.body.opponentScore) {
opponentName = request.body.opponent;
opponentScore = request.body.opponentScore;
}
const res = await fetch('http://localhost:3003/', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ p1: userId, p2: opponentName, p1Score: request.body.myScore, p2Score: opponentScore }) });
if (!res.ok) {
throw new Error('Internal server error');
}
const data = await res.json();
addMatch.run(request.body.game, request.body.date, userId, opponentName, data.id);
}
export async function pMatchHistory(request, reply, fastify, getUserInfo, addMatch, incWinsPong, incLossesPong, incWinsTetris, incLossesTetris) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
if (request.body.game !== 'pong' && request.body.game !== 'tetris') {
return reply.code(400).send({ error: "Specified game does not exist" });
}
if (request.body.game === 'pong' && (!request.body.opponent || !request.body.opponentScore)) {
return reply.code(400).send({ error: "Game requires two players" });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
if (request.body.opponent) {
if (!getUserInfo.get(request.body.opponent)) {
return reply.code(404).send({ error: "Opponent does not exist" });
}
if (request.body.opponent === userId) {
return reply.code(400).send({ error: "Do you have dementia ? You cannot have played a match against yourself gramps" });
}
}
await fetchSave(request, reply, userId, addMatch);
if (request.body.game === 'pong') {
if (request.body.myScore > request.body.opponentScore) {
incWinsPong.run(userId);
incLossesPong.run(request.body.opponent);
} else if (request.body.myScore < request.body.opponentScore) {
incWinsPong.run(request.body.opponent);
incLossesPong.run(userId);
}
}
else if (request.body.game === 'tetris' && request.body.opponent && request.body.opponentScore) {
if (request.body.myScore > request.body.opponentScore) {
incWinsTetris.run(userId);
incLossesTetris.run(request.body.opponent);
} else if (request.body.myScore < request.body.opponentScore) {
incWinsTetris.run(request.body.opponent);
incLossesTetris.run(userId);
}
}
return reply.code(200).send({ msg: "Match successfully saved to the blockchain" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

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

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

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

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

@ -0,0 +1,48 @@
import sharp from 'sharp';
export async function uAvatar(request, reply, fastify, getUserInfo, setAvatarId, getAvatarId, deleteAvatarId, postImage, deleteImage) {
try {
const userId = request.params.userId;
if (request.user !== userId && request.user !== 'admin') {
return reply.code(401).send({ error: 'Unauthorized' });
}
if (!getUserInfo.get(userId)) {
return reply.code(404).send({ error: "User does not exist" });
}
deleteAvatarId.run(userId);
const parts = request.parts();
for await (const part of parts) {
if (part.file) {
let size = 0;
const chunks = [];
for await (const chunk of part.file) {
size += chunk.length;
chunks.push(chunk);
}
if (size === 5 * 1024 * 1024 + 1) {
return reply.code(400).send({ error: "File too large" });
}
const buffer = Buffer.concat(chunks);
if (!part.filename || part.filename.trim() === '') {
return reply.code(400).send({ error: "Missing filename" });
}
if (!part.mimetype || part.mimetype.trim() === '') {
return reply.code(400).send({ error: "Missing mimetype" });
}
const webpBuffer = await sharp(buffer).toFormat('webp').toBuffer();
const imageId = postImage.run(part.filename, part.mimetype, webpBuffer);
const oldImageId = getAvatarId.get(userId);
if (oldImageId.avatarId !== -1) {
deleteImage.run(oldImageId.avatarId);
deleteAvatarId.run(userId);
}
setAvatarId.run(imageId.lastInsertRowid, userId);
return reply.code(200).send({ msg: "Avatar modified successfully" });
}
}
return reply.code(400).send({ error: "No avatar modified" });
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal server error" });
}
}

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

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

View File

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

View File

@ -55,7 +55,47 @@ export default class extends Aview {
}
async run() {
dragElement(document.getElementById("window"));
dragElement(document.getElementById("window"));
const totpVerify = async () => {
const username = (document.getElementById("username") as HTMLInputElement).value;
const password = (document.getElementById("password") as HTMLInputElement).value;
const totpPin = (document.getElementById('totpPin') as HTMLInputElement).value;
const idWindow = (document.getElementById('2fa-popup-content') as HTMLInputElement);
try {
const data_req = await fetch("http://localhost:3001/login", {
method: "POST",
headers: { "Content-Type": "application/json", },
credentials: "include",
body: JSON.stringify({ user: username, password: password, token: totpPin }),
});
if (data_req.status === 200) {
isLogged();
navigationManager("/");
} else if (data_req.status === 401) {
const data = await data_req.json();
if (!document.getElementById("error-totp")) {
const error = document.createElement("p");
error.innerHTML = data.error;
error.classList.add("text-red-700", "dark:text-red-500");
idWindow.appendChild(error);
} else {
const error = document.getElementById("error-totp") as HTMLParagraphElement;
error.innerHTML = data.error;
}
} else {
console.log(data_req.status)
console.log(await data_req.json())
// throw new Error("invalid response");
}
} catch (error) {
console.error(error);
}
}
const login = async () => {
const username = (document.getElementById("username") as HTMLInputElement).value;
const password = (document.getElementById("password") as HTMLInputElement).value;
@ -73,11 +113,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
{

View File

@ -138,6 +138,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) {
if (match.score.p2 != undefined)
@ -202,7 +203,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"));

View File

@ -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 `
<a class="menu-default-button inline-flex items-center justify-center" href="/login" data-link>login</a>
<a class="menu-default-button inline-flex items-center justify-center" href="/register" data-link>register</a>
`;
}
document.getElementById("menu-bottom-div").classList.remove("hidden");
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}`, {
@ -82,6 +82,6 @@ export default class extends Aview {
else
console.error("logout failed");
});
});
document.getElementById("profile-items").innerHTML = await getMainHTML();
}
}

View File

@ -5,8 +5,7 @@ import { isLogged, navigationManager } from "../main.ts"
export default class extends Aview {
constructor()
{
constructor() {
super();
this.setTitle("register");
setOnekoState("default");
@ -55,7 +54,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;
@ -69,8 +68,7 @@ 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",
@ -80,22 +78,23 @@ export default class extends Aview {
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");
}
}
};

View File

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

View File

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

View File

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

View File

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

View File

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