auth reload and better tour handling

This commit is contained in:
Maieul BOYER 2026-01-13 16:48:20 +01:00 committed by apetitco
parent deb391807a
commit 30540bfe92
7 changed files with 188 additions and 102 deletions

View file

@ -4,7 +4,6 @@ import cookie from "js-cookie";
import { ensureWindowState, isNullish } from "@app/utils";
import { handleRoute, navigateTo } from "./routing";
cookie.remove("pkce");
const headerProfile =
document.querySelector<HTMLDivElement>("#header-profile")!;
@ -29,6 +28,7 @@ declare module "ft_state" {
user: User | null;
_headerProfile: boolean;
_reloadOnAuthChange: boolean;
lastAuthChange: number;
}
}
@ -41,12 +41,16 @@ export function isLogged(): boolean {
}
export function setUser(newUser: User | null) {
let sendEvent = (window.__state.user?.id !== newUser?.id);
let sendEvent = window.__state.user?.id !== newUser?.id;
window.__state.user = newUser;
updateHeaderProfile();
if (sendEvent) {
const event = new CustomEvent("ft:userChange", { detail: isNullish(newUser) ? null : { id: newUser.id, name: newUser.name } });
const event = new CustomEvent("ft:userChange", {
detail: isNullish(newUser)
? null
: { id: newUser.id, name: newUser.name },
});
document.dispatchEvent(event);
}
}
@ -89,7 +93,9 @@ window.__state._headerProfile ??= false;
if (!window.__state._headerProfile) {
headerProfile.addEventListener("click", () => {
if (window.__state.user === null) {
navigateTo(`/login?returnTo=${encodeURIComponent(window.location.pathname)}`);
navigateTo(
`/login?returnTo=${encodeURIComponent(window.location.pathname)}`,
);
} else {
navigateTo("/profile");
}
@ -99,7 +105,12 @@ if (!window.__state._headerProfile) {
window.__state._reloadOnAuthChange ??= false;
if (!window.__state._reloadOnAuthChange) {
document.addEventListener("ft:userChange", () => handleRoute());
document.addEventListener("ft:userChange", () => {
// if the last forced auth change is less than 1000 sec old -> we do nothing
if (Date.now() - (window.__state.lastAuthChange ?? Date.now()) < 1000)
return;
handleRoute();
});
window.__state._reloadOnAuthChange = true;
}
updateHeaderProfile();

View file

@ -11,11 +11,13 @@ import client from "@app/api";
import cuteCat from "./cuteCat.png";
import loggedInHtml from "./alreadyLoggedin.html?raw";
import totpHtml from "./totp.html?raw";
import { isNullish } from "@app/utils";
import { ensureWindowState, isNullish } from "@app/utils";
import { showError, showInfo, showSuccess } from "@app/toast";
import { updateUser } from "@app/auth";
const TOTP_LENGTH = 6;
ensureWindowState();
window.__state.lastAuthChange = Date.now();
async function handleOtp(
app: HTMLElement,
@ -86,6 +88,7 @@ async function handleOtp(
});
if (res.kind === "success") {
window.__state.lastAuthChange = Date.now();
Cookie.set("token", res.payload.token, {
path: "/",
sameSite: "lax",
@ -178,6 +181,7 @@ async function handleLogin(
});
switch (res.kind) {
case "success": {
window.__state.lastAuthChange = Date.now();
Cookie.set("token", res.payload.token, {
path: "/",
sameSite: "lax",
@ -219,6 +223,7 @@ async function handleLogin(
});
switch (res.kind) {
case "success": {
window.__state.lastAuthChange = Date.now();
Cookie.set("token", res.payload.token, {
path: "/",
sameSite: "lax",

View file

@ -8,7 +8,13 @@ import {
import authHtml from "./pong.html?raw";
import tourScoresHtml from "./tourTable.html?raw";
import io from "socket.io-client";
import { JoinRes, type CSocket, type GameMove, type GameUpdate, type TourInfo } from "./socket";
import {
JoinRes,
type CSocket,
type GameMove,
type GameUpdate,
type TourInfo,
} from "./socket";
import { showError, showInfo, showSuccess } from "@app/toast";
import { getUser as getSelfUser, type User } from "@app/auth";
import { isNullish } from "@app/utils";
@ -86,6 +92,7 @@ function pongClient(
// [ ] shape sock
// - [ ] joinGame (guid) -> ["ok"|"no, dont ever talk to my kid or me ever again you creep"];
// - [ ] launch newgame evt?
let inTournament = false;
return {
html: authHtml,
@ -245,21 +252,19 @@ function pongClient(
// join game
// ---
if (game_req_join != null) {
socket.emit('joinGame', game_req_join,
(res : JoinRes) => {
switch (res) {
case JoinRes.yes :
showInfo('JoinRes = yes');
quitChat();
break;
case JoinRes.no :
showInfo('JoinRes = no');
break;
default:
showError('JoinRes switch fail:' + res);
}
socket.emit("joinGame", game_req_join, (res: JoinRes) => {
switch (res) {
case JoinRes.yes:
showInfo("JoinRes = yes");
quitChat();
break;
case JoinRes.no:
showInfo("JoinRes = no");
break;
default:
showError("JoinRes switch fail:" + res);
}
)
});
game_req_join = null;
}
// ---
@ -300,20 +305,33 @@ function pongClient(
playerL.innerText = "";
currentGame = null;
}
let render_tour_score_once = false;
const renderTournamentScores = (info: TourInfo) => {
let players = info.players.sort((l, r) => r.score - l.score);
const medals = ["🥇", "🥈", "🥉"];
tour_scores.innerHTML = tourScoresHtml;
if (!render_tour_score_once)
{
tour_scores.innerHTML = tourScoresHtml;
render_tour_score_once = true;
}
let table = tour_scores.querySelector("#tour-score-body");
if (table)
table.innerHTML = players.map((player, idx) =>
`<tr class="${player.id === user.id ? "bg-amber-400 hover:bg-amber-500" : "hover:bg-gray-50"}" key="${player.id}">
<td class="px-4 py-2 text-sm text-gray-800 text-center border-b font-semibold min-w-100px">${idx < medals.length ? `<span class="font-lg">${medals[idx]}</span>` : ''}${player.name}</td>
<td class="px-4 py-2 text-sm text-gray-800 text-center border-b font-bold min-w-100px">${player.score}</td>
</tr>`)
let table_shadow = document.createElement("tbody");
if (table) {
table_shadow.innerHTML = players
.map(
(player, idx) =>
`<tr class="${player.id === user.id ? "bg-amber-400 hover:bg-amber-500" : "hover:bg-gray-50"}" data-id="${player.id}">
<td class="px-4 py-2 text-sm text-gray-800 text-center border-b font-semibold min-w-100px"><span class="font-lg medal">${idx < medals.length ? medals[idx] : ""}</span>${player.name}</td>
<td class="px-4 py-2 text-sm text-gray-800 text-center border-b font-bold min-w-100px">${player.score}</td>
</tr>`,
)
.join("");
if (table_shadow.innerHTML !== table.innerHTML) {
table.innerHTML = table_shadow.innerHTML;
}
}
};
// TODO: REMOVE THIS
@ -371,6 +389,10 @@ function pongClient(
// btn setup
queueBtn.addEventListener("click", () => {
if (inTournament) {
showError("You can't queue up currently !");
return;
}
if (queueBtn.innerText !== QueueState.Iddle) {
if (queueBtn.innerText === QueueState.InQueu) {
socket.emit("dequeue");
@ -384,11 +406,10 @@ function pongClient(
LocalGameBtn.addEventListener("click", () => {
if (
queueBtn.innerText !== QueueState.Iddle ||
currentGame !== null
currentGame !== null ||
inTournament
) {
showError(
"cant launch a local game while in queue/in game",
);
showError("cant launch a game currently");
return;
}
socket.emit("localGame");
@ -467,6 +488,7 @@ function pongClient(
updateCurrentGame(state);
render(state);
tour_scores.classList.add("hidden");
queueBtn.innerText = QueueState.InGame;
queueBtn.style.color = "red";
batLeft.style.backgroundColor = DEFAULT_COLOR;
@ -487,14 +509,17 @@ function pongClient(
queueBtn.style.color = "white";
if (!isNullish(currentGame)) {
let end_txt: string = '';
if ((user.id === currentGame.game.left.id && winner === 'left') ||
(user.id === currentGame.game.right.id && winner === 'right'))
end_txt = 'you won! #yippe';
else
end_txt = 'you lost #sadge';
let end_txt: string = "";
if (
(user.id === currentGame.game.left.id &&
winner === "left") ||
(user.id === currentGame.game.right.id &&
winner === "right")
)
end_txt = "you won! #yippe";
else end_txt = "you lost #sadge";
if (currentGame.spectating)
end_txt = `${winner === 'left' ? currentGame.playerL.name : currentGame.playerR.name} won #gg`;
end_txt = `${winner === "left" ? currentGame.playerL.name : currentGame.playerR.name} won #gg`;
end_scr.innerText = end_txt;
end_scr.classList.remove("hidden");
setTimeout(() => {
@ -528,13 +553,16 @@ function pongClient(
let imOwner = s.ownerId === user.id;
switch (s.state) {
case "ended":
inTournament = false;
tournamentBtn.innerText = TourBtnState.AbleToCreate;
break;
case "playing":
inTournament = true;
tournamentBtn.innerText = TourBtnState.Started;
tour_infos.innerText = `${TourInfoState.Running} ${s.players.length}👤 ${s.remainingMatches ?? "?"}▮•▮`;
break;
case "prestart":
inTournament = true;
tour_infos.innerText = `${imOwner ? TourInfoState.Owner : weIn ? TourInfoState.Registered : TourInfoState.NotRegisted} ${s.players.length}👤 ?▮•▮`;
if (imOwner) {
tournamentBtn.innerText = TourBtnState.AbleToStart;

View file

@ -5,10 +5,10 @@
<table class="min-w-full border-collapse table-fixed">
<thead class="sticky top-0 z-10 bg-gray-100">
<tr>
<th class="px-4 py-2 text-right text-sm font-semibold text-gray-700 border-b">
<th class="px-4 py-2 text-center text-sm font-semibold text-gray-700 border-b">
Name
</th>
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700 border-b">
<th class="px-4 py-2 text-center text-sm font-semibold text-gray-700 border-b">
Score
</th>
</tr>

View file

@ -1,26 +1,38 @@
import { addRoute, setTitle, navigateTo, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing";
import {
addRoute,
setTitle,
navigateTo,
type RouteHandlerParams,
type RouteHandlerReturn,
} from "@app/routing";
import { showError, showInfo, showSuccess } from "@app/toast";
import page from './signin.html?raw';
import client from '@app/api'
import page from "./signin.html?raw";
import client from "@app/api";
import { updateUser } from "@app/auth";
import Cookie from 'js-cookie';
import loggedInHtml from './alreadyLoggedin.html?raw';
import { isNullish } from "@app/utils";
import cuteCat from './cuteCat.png';
import Cookie from "js-cookie";
import loggedInHtml from "./alreadyLoggedin.html?raw";
import { ensureWindowState, isNullish } from "@app/utils";
import cuteCat from "./cuteCat.png";
const MSG_KEY_TO_STRING = {
'signin.failed.username.existing': 'Username already exists',
'signin.failed.username.toolong': 'Username is too short',
'signin.failed.username.tooshort': 'Username is too long',
'signin.failed.username.invalid': 'Username is invalid',
'signin.failed.password.toolong': 'Password is too long',
'signin.failed.password.tooshort': 'Password is too short',
'signin.failed.password.invalid': 'Password is invalid',
'signin.failed.generic': 'Unknown Error',
"signin.failed.username.existing": "Username already exists",
"signin.failed.username.toolong": "Username is too short",
"signin.failed.username.tooshort": "Username is too long",
"signin.failed.username.invalid": "Username is invalid",
"signin.failed.password.toolong": "Password is too long",
"signin.failed.password.tooshort": "Password is too short",
"signin.failed.password.invalid": "Password is invalid",
"signin.failed.generic": "Unknown Error",
};
async function handleSignin(_url: string, _args: RouteHandlerParams): Promise<RouteHandlerReturn> {
setTitle('Signin')
ensureWindowState();
window.__state.lastAuthChange = Date.now();
async function handleSignin(
_url: string,
_args: RouteHandlerParams,
): Promise<RouteHandlerReturn> {
setTitle("Signin");
let user = await updateUser();
const urlParams = new URLSearchParams(window.location.search);
const returnTo = urlParams.get("returnTo");
@ -57,43 +69,67 @@ async function handleSignin(_url: string, _args: RouteHandlerParams): Promise<Ro
}
return {
html: page, postInsert: async (app) => {
const fSignin = document.querySelector<HTMLFormElement>('form#signin-form');
html: page,
postInsert: async (app) => {
const fSignin =
document.querySelector<HTMLFormElement>("form#signin-form");
if (fSignin === null)
return showError('Error while rendering the page: no form found');
fSignin.addEventListener('submit', async function(e: SubmitEvent) {
return showError(
"Error while rendering the page: no form found",
);
fSignin.addEventListener("submit", async function(e: SubmitEvent) {
e.preventDefault();
let form = e.target as (HTMLFormElement | null);
if (form === null)
return showError('Failed to send form...');
let formData = Object.fromEntries((new FormData(form)).entries());
if (!('login' in formData) || typeof formData['login'] !== 'string' || (formData['login'] as string).length === 0)
return showError('Please enter a Login');
if (!('password' in formData) || typeof formData['password'] !== 'string' || (formData['password'] as string).length === 0)
return showError('Please enter a Password');
let form = e.target as HTMLFormElement | null;
if (form === null) return showError("Failed to send form...");
let formData = Object.fromEntries(new FormData(form).entries());
if (
!("login" in formData) ||
typeof formData["login"] !== "string" ||
(formData["login"] as string).length === 0
)
return showError("Please enter a Login");
if (
!("password" in formData) ||
typeof formData["password"] !== "string" ||
(formData["password"] as string).length === 0
)
return showError("Please enter a Password");
try {
const res = await client.signin({ loginRequest: { name: formData.login, password: formData.password } });
const res = await client.signin({
loginRequest: {
name: formData.login,
password: formData.password,
},
});
switch (res.kind) {
case 'success': {
Cookie.set('token', res.payload.token, { path: '/', sameSite: 'lax' });
case "success": {
window.__state.lastAuthChange = Date.now();
Cookie.set("token", res.payload.token, {
path: "/",
sameSite: "lax",
});
let user = await updateUser();
if (user === null)
return showError('Failed to get user: no user ?');
navigateTo(returnTo !== null ? returnTo : '/')
return showError(
"Failed to get user: no user ?",
);
navigateTo(returnTo !== null ? returnTo : "/");
break;
}
case 'failed': {
showError(`Failed to signin: ${MSG_KEY_TO_STRING[res.msg]}`);
case "failed": {
showError(
`Failed to signin: ${MSG_KEY_TO_STRING[res.msg]}`,
);
}
}
} catch (e) {
console.error("Signin error:", e);
showError('Failed to signin: Unknown error');
showError("Failed to signin: Unknown error");
}
});
}
}
};
},
};
}
addRoute("/signin", handleSignin, { bypass_auth: true });
addRoute('/signin', handleSignin, { bypass_auth: true })

View file

@ -82,12 +82,12 @@ const route: FastifyPluginAsync = async (fastify): Promise<void> => {
left: {
score: v.left.score,
id: v.left.id,
name: `${v.nameL}-left`,
name: `${v.nameL}`,
},
right: {
score: v.right.score,
id: v.right.id,
name: `${v.nameR}-right`,
name: `${v.nameR}`,
},
local: v.local,
date: v.time.toString(),

View file

@ -77,23 +77,21 @@ class StateI {
const u1 = this.users.get(id1);
const u2 = this.users.get(id2);
if (isNullish(u1) || isNullish(u2)) return null;
this.fastify.log.info({
msg: 'init new game',
user1: u1.id,
user2: u2.id,
user1: id1,
user2: id2,
});
if (g === null) g = new Pong(u1.id, u2.id);
if (g === null) g = new Pong(id1, id2);
const iState: GameUpdate = StateI.getGameUpdateData(gameId, g);
u1.socket.emit('newGame', iState);
u2.socket.emit('newGame', iState);
u1?.socket.emit('newGame', iState);
u2?.socket.emit('newGame', iState);
g.rdy_timer = Date.now();
this.games.set(gameId, g);
u1.currentGame = gameId;
u2.currentGame = gameId;
if (u1) { u1.currentGame = gameId; }
if (u2) { u2.currentGame = gameId; }
g.gameUpdate = setInterval(() => {
g.tick();
@ -102,13 +100,21 @@ class StateI {
g.ready_checks[0] === true &&
g.ready_checks[1] === true
) {
u1.socket.emit('rdyEnd');
u2.socket.emit('rdyEnd');
if (u1) {
u1.socket.emit('rdyEnd');
}
if (u2) {
u2.socket.emit('rdyEnd');
}
g.sendSig = true;
}
if (g.ready_checks[0] === true && g.ready_checks[1] === true) {
this.gameUpdate(gameId, u1.socket);
this.gameUpdate(gameId, u2.socket);
if (u1) {
this.gameUpdate(gameId, u1.socket);
}
if (u2) {
this.gameUpdate(gameId, u2.socket);
}
}
if (g.checkWinner() !== null) {
this.cleanupGame(gameId, g);
@ -376,14 +382,14 @@ class StateI {
this.cleanupUser(sock);
}
private tryJoinGame(g_id : string, sock : SSocket) : JoinRes {
const game_id : PongGameId = g_id as PongGameId;
private tryJoinGame(g_id: string, sock: SSocket): JoinRes {
const game_id: PongGameId = g_id as PongGameId;
if (this.games.has(game_id) === false) {
this.fastify.log.warn('gameId:' + g_id + ' is unknown!');
return (JoinRes.no);
}
const game : Pong = this.games.get(game_id)!;
const game: Pong = this.games.get(game_id)!;
if (game.local === true || (game.userLeft !== sock.authUser.id && game.userRight !== sock.authUser.id)) {
this.fastify.log.warn('user trying to connect to a game he\'s not part of: gameId:' + g_id + ' userId:' + sock.authUser.id);
return (JoinRes.no);
@ -426,7 +432,7 @@ class StateI {
socket.on('gameMove', (e) => this.gameMove(socket, e));
socket.on('localGame', () => this.newLocalGame(socket));
socket.on('joinGame', (g_id, ack) => {return (ack(this.tryJoinGame(g_id, socket)));});
socket.on('joinGame', (g_id, ack) => { return (ack(this.tryJoinGame(g_id, socket))); });
// todo: allow passing nickname
socket.on('tourRegister', () =>
this.registerForTournament(socket, null),
@ -587,4 +593,4 @@ export let State: StateI = undefined as unknown as StateI;
export function newState(f: FastifyInstance) {
State = new StateI(f);
}
}